December 6th, 2024

Goal

Enhanced the gene viewer in the View Target module. Bugs Fixed.

Hypothesis

If the gene viewer in the View Target module is enhanced, it will make it easier for users to understand where the guides are located within the target genes.

If the bugs are resolved, users will be able to operate the program without experiencing crashes.

Expected Results

Results

Bugs Fixed

Special thanks to David for stress-testing the program last week. The following issues have been resolved:

Next Steps

My next step is to address any bugs that David may encounter during testing. I will focus on stabilizing the modules with outstanding issues and proceed with packaging the app for Windows. Additionally, I will plan the implementation of the Microbiome Analysis module in collaboration with David.

Files changed (42) hide show
  1. Dockerfile +46 -0
  2. README.md +75 -0
  3. docker-compose.yml +15 -0
  4. requirements.txt +18 -0
  5. scripts/mac.spec +0 -69
  6. src/controllers/FindTargetsController.py +55 -40
  7. src/controllers/HomeWindowController.py +153 -74
  8. src/controllers/MainWindowController.py +15 -33
  9. src/controllers/MultitargetingWindowController.py +26 -55
  10. src/controllers/NCBIWindowController.py +29 -21
  11. src/controllers/NewGenomeWindowController.py +57 -1
  12. src/controllers/StartupWindowController.py +9 -4
  13. src/controllers/ViewTargetsController.py +373 -266
  14. src/models/AnnotationParser.py +115 -176
  15. src/models/CSPRparser.py +8 -22
  16. src/models/DatabaseManager.py +37 -22
  17. src/models/FindTargetsModel.py +79 -92
  18. src/models/GlobalSettings.py +264 -114
  19. src/models/HomeWindowModel.py +1 -1
  20. src/models/MultitargetingWindowModel.py +25 -3
  21. src/models/NCBIWindowModel.py +526 -148
  22. src/models/NewEndonucleaseModel.py +77 -52
  23. src/models/NewGenomeWindowModel.py +20 -10
  24. src/models/OffTarget/local_output.txt +0 -4
  25. src/models/StartupWindowModel.py +5 -1
  26. src/models/ViewTargetsModel.py +179 -130
  27. src/ui/ncbi.ui +32 -22
  28. src/ui/view_targets.ui +81 -74
  29. src/views/CloseableTabWidget.py +38 -25
  30. src/views/DNAFeatureViewer.py +1227 -0
  31. src/views/ExportSelectedgRNAsView.py +11 -1
  32. src/views/FindTargetsView.py +63 -40
  33. src/views/GenBankParse.py +0 -82
  34. src/views/HomeWindowView.py +22 -3
  35. src/views/LoadingDialog.py +83 -0
  36. src/views/MainWindowUI.py +0 -152
  37. src/views/MainWindowView copy.py +0 -265
  38. src/views/MainWindowView.py +47 -219
  39. src/views/MultitargetingWindowView.py +37 -15
  40. src/views/NCBIWindowView.py +10 -2
  41. src/views/StartupWindowView.py +1 -6
  42. src/views/ViewTargetsView.py +130 -26
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM --platform=linux/amd64 python:3.11.9-slim
2
+
3
+ # Install system dependencies including X11, Qt dependencies, and required Linux libraries
4
+ RUN apt-get update && apt-get install -y \
5
+ libgl1-mesa-glx \
6
+ libx11-xcb1 \
7
+ libxcb-icccm4 \
8
+ libxcb-image0 \
9
+ libxcb-keysyms1 \
10
+ libxcb-randr0 \
11
+ libxcb-render-util0 \
12
+ libxcb-shape0 \
13
+ libxcb-xfixes0 \
14
+ libxcb-xinerama0 \
15
+ libxkbcommon-x11-0 \
16
+ xvfb \
17
+ libegl1 \
18
+ libopengl0 \
19
+ libxcb-cursor0 \
20
+ qt6-base-dev \
21
+ glibc-source \
22
+ build-essential \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ # Set working directory
26
+ WORKDIR /app
27
+
28
+ # Copy the entire application
29
+ COPY . .
30
+
31
+ # Make sure the SeqFinder executable has correct permissions
32
+ RUN chmod +x /app/src/SeqFinder/Casper_Seq_Finder_Lin
33
+
34
+ # Install Python dependencies
35
+ RUN pip install --no-cache-dir -r requirements.txt
36
+
37
+ # Set environment variables for Qt
38
+ ENV QT_QPA_PLATFORM=xcb
39
+ ENV XDG_RUNTIME_DIR=/tmp/runtime-root
40
+ ENV DISPLAY=:0
41
+
42
+ # Create runtime directory
43
+ RUN mkdir -p /tmp/runtime-root && chmod 0700 /tmp/runtime-root
44
+
45
+ # Command to run the application
46
+ CMD ["python3", "src/main.py"]
README.md CHANGED
@@ -20,3 +20,78 @@ Thank you for your interest in CASPER. Our packaged releases for Windows 10 and
20
  CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
21
 
22
  NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
21
 
22
  NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
23
+
24
+ ### Docker Installation (macOS)
25
+
26
+ #### Prerequisites
27
+ 1. Install Docker Desktop for Mac
28
+ ```bash
29
+ brew install --cask docker
30
+ ```
31
+
32
+ 2. Install XQuartz
33
+ ```bash
34
+ brew install --cask xquartz
35
+ ```
36
+
37
+ 3. Configure XQuartz:
38
+ ```bash
39
+ # Start XQuartz
40
+ open -a XQuartz
41
+
42
+ # In XQuartz Preferences → Security:
43
+ # - Check "Allow connections from network clients"
44
+ # - Restart XQuartz after changing settings
45
+ ```
46
+
47
+ 4. Set up X11 forwarding (run these commands each time before starting the app):
48
+ ```bash
49
+ # Start XQuartz if not running
50
+ open -a XQuartz
51
+
52
+ # Get your IP address
53
+ export IP=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}')
54
+
55
+ # Set up permissions (use your actual IP)
56
+ xhost + $IP
57
+
58
+ # Clean up any old containers
59
+ docker-compose down
60
+ ```
61
+
62
+ 5. Run CASPER:
63
+ ```bash
64
+ # First time or after making changes:
65
+ docker-compose up --build
66
+
67
+ # Subsequent runs (without rebuilding):
68
+ docker-compose up
69
+
70
+ # Or run in background:
71
+ docker-compose up -d
72
+ ```
73
+
74
+ #### Troubleshooting
75
+ - If the app doesn't start:
76
+ ```bash
77
+ # Stop all containers
78
+ docker-compose down
79
+
80
+ # Remove old containers and images
81
+ docker system prune -f
82
+
83
+ # Restart XQuartz
84
+ killall Xquartz
85
+ open -a XQuartz
86
+
87
+ # Set up X11 again
88
+ xhost + localhost
89
+
90
+ # Try running again
91
+ docker-compose up --build
92
+ ```
93
+
94
+ - If you still have issues:
95
+ - Make sure Docker Desktop is running
96
+ - Try restarting your computer
97
+ - Run `docker-compose logs` to see detailed error messages
docker-compose.yml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ casperapp:
5
+ platform: linux/amd64
6
+ build: .
7
+ environment:
8
+ - DISPLAY=host.docker.internal:0
9
+ extra_hosts:
10
+ - "host.docker.internal:host-gateway"
11
+ volumes:
12
+ - /tmp/.X11-unix:/tmp/.X11-unix
13
+ - .:/app
14
+ network_mode: "bridge"
15
+ privileged: true
requirements.txt CHANGED
@@ -1,30 +1,48 @@
 
 
1
  beautifulsoup4==4.12.3
2
  biopython==1.84
 
 
3
  contourpy==1.3.0
4
  cycler==0.12.1
5
  darkdetect==0.7.1
 
6
  fonttools==4.53.1
 
 
 
7
  joblib==1.4.2
8
  kiwisolver==1.4.7
9
  lxml==5.3.0
 
10
  matplotlib==3.9.2
 
11
  mplcursors==0.5.3
12
  numpy==2.1.1
13
  packaging==24.1
14
  pandas==2.2.2
15
  pillow==10.4.0
 
 
 
 
16
  pyparsing==3.1.4
17
  PyQt6==6.7.1
18
  PyQt6-Qt6==6.7.2
19
  PyQt6_sip==13.8.0
20
  pyqtdarktheme==2.1.0
 
21
  python-dateutil==2.9.0.post0
22
  python-dotenv==1.0.1
23
  pytz==2024.1
24
  PyYAML==6.0.2
 
25
  scikit-learn==1.5.2
26
  scipy==1.14.1
27
  six==1.16.0
28
  soupsieve==2.6
29
  threadpoolctl==3.5.0
 
30
  tzdata==2024.1
 
 
1
+ altgraph==0.17.4
2
+ astroid==3.3.5
3
  beautifulsoup4==4.12.3
4
  biopython==1.84
5
+ certifi==2024.8.30
6
+ charset-normalizer==3.4.0
7
  contourpy==1.3.0
8
  cycler==0.12.1
9
  darkdetect==0.7.1
10
+ dill==0.3.9
11
  fonttools==4.53.1
12
+ graphviz==0.20.3
13
+ idna==3.10
14
+ isort==5.13.2
15
  joblib==1.4.2
16
  kiwisolver==1.4.7
17
  lxml==5.3.0
18
+ macholib==1.16.3
19
  matplotlib==3.9.2
20
+ mccabe==0.7.0
21
  mplcursors==0.5.3
22
  numpy==2.1.1
23
  packaging==24.1
24
  pandas==2.2.2
25
  pillow==10.4.0
26
+ platformdirs==4.3.6
27
+ pyinstaller==6.11.1
28
+ pyinstaller-hooks-contrib==2024.10
29
+ pylint==3.3.1
30
  pyparsing==3.1.4
31
  PyQt6==6.7.1
32
  PyQt6-Qt6==6.7.2
33
  PyQt6_sip==13.8.0
34
  pyqtdarktheme==2.1.0
35
+ python-call-graph==2.1.2
36
  python-dateutil==2.9.0.post0
37
  python-dotenv==1.0.1
38
  pytz==2024.1
39
  PyYAML==6.0.2
40
+ requests==2.32.3
41
  scikit-learn==1.5.2
42
  scipy==1.14.1
43
  six==1.16.0
44
  soupsieve==2.6
45
  threadpoolctl==3.5.0
46
+ tomlkit==0.13.2
47
  tzdata==2024.1
48
+ urllib3==2.2.3
scripts/mac.spec DELETED
@@ -1,69 +0,0 @@
1
- block_cipher = None
2
-
3
- a = Analysis(['src/main.py'],
4
- pathex=['src'],
5
- datas=[
6
- ('assets', 'assets'),
7
- ('config', 'config'),
8
- ('logs', 'logs'),
9
- ('src', 'src'),
10
- ('genomeBrowserTemplate.html', '.'),
11
- ],
12
- hiddenimports=[],
13
- hookspath=[],
14
- runtime_hooks=[],
15
- excludes=[],
16
- win_no_prefer_redirects=False,
17
- win_private_assemblies=False,
18
- cipher=block_cipher,
19
- noarchive=False)
20
-
21
- pyz = PYZ(a.pure, a.zipped_data,
22
- cipher=block_cipher)
23
-
24
- exe = EXE(pyz,
25
- a.scripts,
26
- [],
27
- exclude_binaries=True,
28
- name='CASPERapp',
29
- debug=False,
30
- bootloader_ignore_signals=False,
31
- strip=False,
32
- upx=True,
33
- console=False,
34
- disable_windowed_traceback=False,
35
- target_arch=None,
36
- codesign_identity=None,
37
- entitlements_file=None,
38
- icon='assets/CASPER_icon.icns')
39
-
40
- coll = COLLECT(exe,
41
- a.binaries,
42
- a.zipfiles,
43
- a.datas,
44
- strip=False,
45
- upx=True,
46
- upx_exclude=[],
47
- name='CASPERapp')
48
-
49
- app = BUNDLE(coll,
50
- name='CASPERapp.app',
51
- icon='assets/CASPER_icon.icns',
52
- version='2.0.1',
53
- bundle_identifier=None)
54
-
55
- # 1. Have the mac.spec in the app directory
56
- # 2. pyinstaller mac.spec
57
- # 3. mkdir -p dist/dmg
58
- # 4. rm -r dist/dmg/*
59
- # 5. Manual copy of the app into dist/dmg
60
- # 6. create-dmg \
61
- # --volname "CASPERapp" \
62
- # --window-pos 200 120 \
63
- # --window-size 600 300 \
64
- # --icon-size 100 \
65
- # --icon "CASPERapp.app" 175 120 \
66
- # --hide-extension "CASPERapp.app" \
67
- # --app-drop-link 425 120 \
68
- # "dist/CASPERapp.dmg" \
69
- # "dist/dmg/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/controllers/FindTargetsController.py CHANGED
@@ -2,8 +2,8 @@ from models.FindTargetsModel import FindTargetsModel
2
  from utils.ui import show_error
3
  from views.FindTargetsView import FindTargetsView
4
  from PyQt6.QtWidgets import QMessageBox
5
- from PyQt6.QtCore import QTimer
6
- import time
7
 
8
  class FindTargetsController:
9
  def __init__(self, global_settings):
@@ -14,6 +14,7 @@ class FindTargetsController:
14
  self.endonuclease = None
15
  self._input_data = None
16
  self._current_annotation_file = None
 
17
 
18
  # Connect to annotation file changes
19
  self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
@@ -45,13 +46,10 @@ class FindTargetsController:
45
  def find_targets(self, input_data):
46
  """Process input data and update existing view or create new one"""
47
  try:
48
- start_time = time.time()
49
-
50
- # Get current annotation file
51
  current_annotation = self.global_settings.get_current_annotation_file()
52
  input_data['annotation_file'] = current_annotation
53
  self._current_annotation_file = current_annotation
54
- self._input_data = input_data.copy() # Store a copy of the input data
55
 
56
  # Process data and update view
57
  self._process_input_data(input_data)
@@ -62,9 +60,6 @@ class FindTargetsController:
62
  if not existing_tab:
63
  main_window.open_new_tab("Find Targets", self)
64
 
65
- total_time = time.time() - start_time
66
- self.global_settings.logger.debug(f"Total time to process find targets: {total_time:.2f} seconds")
67
-
68
  except Exception as e:
69
  self.global_settings.logger.error(f"Error in find_targets: {str(e)}")
70
  raise
@@ -72,28 +67,16 @@ class FindTargetsController:
72
  def _process_input_data(self, input_data):
73
  """Process input data and update view"""
74
  try:
75
- start_time = time.time()
76
-
77
  self.global_settings.logger.debug(f"FindTargetsController processing input data: {input_data}")
78
  self.organism = input_data['organism']
79
  self.endonuclease = input_data['endonuclease']
80
 
81
  # Get new results
82
- search_start = time.time()
83
  results = self.model.find_targets(input_data)
84
- search_time = time.time() - search_start
85
- self.global_settings.logger.debug(f"Time to search: {search_time:.2f} seconds")
86
  self.global_settings.logger.debug(f"Found {len(results) if results else 0} targets")
87
 
88
- # Update view with new results
89
- view_start = time.time()
90
  if results:
91
  self.view.display_results(results)
92
- view_time = time.time() - view_start
93
- self.global_settings.logger.debug(f"Time to update view: {view_time:.2f} seconds")
94
-
95
- total_time = time.time() - start_time
96
- self.global_settings.logger.debug(f"Total time to process data: {total_time:.2f} seconds")
97
 
98
  except Exception as e:
99
  self.global_settings.logger.error(f"Error processing input data: {str(e)}")
@@ -110,28 +93,60 @@ class FindTargetsController:
110
  QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
111
  return
112
 
113
- # Find existing View Targets tab
114
- main_window = self.global_settings.main_window
115
- existing_tab = main_window.find_tab_by_title("View Targets")
116
-
117
- if existing_tab:
118
- # Get the existing controller from main window's tab_widgets
119
- view_targets_controller = main_window.tab_widgets['controllers'].get("View Targets")
120
- if view_targets_controller:
121
- # Update existing view with new targets
122
- view_targets_controller.load_targets(selected_targets, self.organism, self.endonuclease)
123
- # Switch to the existing tab
124
- main_window.view.tab_widget.setCurrentWidget(existing_tab)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  else:
126
- self.logger.error("View Targets controller not found for existing tab")
127
- else:
128
- # Create new View Targets tab if none exists
129
- view_targets_controller = self.global_settings.get_view_targets_window()
130
- view_targets_controller.load_guides(selected_targets, self.organism, self.endonuclease)
131
- main_window.open_new_tab("View Targets", view_targets_controller)
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  except Exception as e:
134
- self.global_settings.logger.error(f"Error in view_targets: {str(e)}")
135
  if self.view:
136
  QMessageBox.critical(self.view, "Error", f"An error occurred while viewing targets: {str(e)}")
137
 
 
2
  from utils.ui import show_error
3
  from views.FindTargetsView import FindTargetsView
4
  from PyQt6.QtWidgets import QMessageBox
5
+ from views.LoadingDialog import LoadingDialog
6
+ from PyQt6.QtWidgets import QApplication
7
 
8
  class FindTargetsController:
9
  def __init__(self, global_settings):
 
14
  self.endonuclease = None
15
  self._input_data = None
16
  self._current_annotation_file = None
17
+ self.logger = self.global_settings.logger
18
 
19
  # Connect to annotation file changes
20
  self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
 
46
  def find_targets(self, input_data):
47
  """Process input data and update existing view or create new one"""
48
  try:
 
 
 
49
  current_annotation = self.global_settings.get_current_annotation_file()
50
  input_data['annotation_file'] = current_annotation
51
  self._current_annotation_file = current_annotation
52
+ self._input_data = input_data.copy()
53
 
54
  # Process data and update view
55
  self._process_input_data(input_data)
 
60
  if not existing_tab:
61
  main_window.open_new_tab("Find Targets", self)
62
 
 
 
 
63
  except Exception as e:
64
  self.global_settings.logger.error(f"Error in find_targets: {str(e)}")
65
  raise
 
67
  def _process_input_data(self, input_data):
68
  """Process input data and update view"""
69
  try:
 
 
70
  self.global_settings.logger.debug(f"FindTargetsController processing input data: {input_data}")
71
  self.organism = input_data['organism']
72
  self.endonuclease = input_data['endonuclease']
73
 
74
  # Get new results
 
75
  results = self.model.find_targets(input_data)
 
 
76
  self.global_settings.logger.debug(f"Found {len(results) if results else 0} targets")
77
 
 
 
78
  if results:
79
  self.view.display_results(results)
 
 
 
 
 
80
 
81
  except Exception as e:
82
  self.global_settings.logger.error(f"Error processing input data: {str(e)}")
 
93
  QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
94
  return
95
 
96
+ # Create loading dialog
97
+ loading_dialog = LoadingDialog(self.view)
98
+ loading_dialog.show()
99
+ loading_dialog.set_progress(0)
100
+ QApplication.processEvents()
101
+
102
+ try:
103
+ # Find existing View Targets tab
104
+ main_window = self.global_settings.main_window
105
+ existing_tab = main_window.find_tab_by_title("View Targets")
106
+
107
+ loading_dialog.set_message("Initializing view targets...", 25)
108
+ QApplication.processEvents()
109
+
110
+ if existing_tab:
111
+ view_targets_controller = main_window.tab_widgets['controllers'].get("View Targets")
112
+ if view_targets_controller:
113
+ loading_dialog.set_message("Loading guides...", 50)
114
+ QApplication.processEvents()
115
+
116
+ # Pass the loading dialog to load_guides
117
+ view_targets_controller.load_guides(
118
+ selected_targets,
119
+ self.organism,
120
+ self.endonuclease,
121
+ loading_dialog=loading_dialog
122
+ )
123
+
124
+ # Switch to the existing tab
125
+ main_window.view.tab_widget.setCurrentWidget(existing_tab)
126
+ else:
127
+ self.logger.error("View Targets controller not found for existing tab")
128
  else:
129
+ loading_dialog.set_message("Creating view targets...", 25)
130
+ QApplication.processEvents()
131
+
132
+ view_targets_controller = self.global_settings.get_view_targets_window()
133
+
134
+ # Pass the loading dialog to load_guides
135
+ view_targets_controller.load_guides(
136
+ selected_targets,
137
+ self.organism,
138
+ self.endonuclease,
139
+ loading_dialog=loading_dialog
140
+ )
141
+
142
+ main_window.open_new_tab("View Targets", view_targets_controller)
143
+
144
+ finally:
145
+ loading_dialog.close()
146
+ QApplication.processEvents()
147
 
148
  except Exception as e:
149
+ self.logger.error(f"Error in view_targets: {str(e)}")
150
  if self.view:
151
  QMessageBox.critical(self.view, "Error", f"An error occurred while viewing targets: {str(e)}")
152
 
src/controllers/HomeWindowController.py CHANGED
@@ -1,12 +1,12 @@
1
- import os
2
- from PyQt6 import QtWidgets, QtCore, uic
3
- from PyQt6.QtWidgets import QMainWindow, QMessageBox
4
  from views.HomeWindowView import HomeWindowView
5
  from models.HomeWindowModel import HomeWindowModel
6
- from utils.ui import show_error, show_message
7
- from PyQt6.QtCore import QObject
8
- from controllers.FindTargetsController import FindTargetsController
9
  from models.DatabaseManager import FileChangeType
 
 
 
10
 
11
  class HomeWindowController:
12
  def __init__(self, global_settings):
@@ -72,8 +72,6 @@ class HomeWindowController:
72
  self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
73
 
74
  # grpStep3
75
- # self.view.radio_button_feature.clicked.connect(self.toggle_annotation)
76
- # self.view.radio_button_position.clicked.connect(self.toggle_annotation)
77
  self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
78
  self.view.radio_button_position.clicked.connect(self.handle_search_type_change)
79
  self.view.radio_button_sequence.clicked.connect(self.handle_search_type_change)
@@ -100,6 +98,13 @@ class HomeWindowController:
100
  "The sequence given is too small. At least 100 characters are required."
101
  )
102
  return
 
 
 
 
 
 
 
103
  self.open_view_targets(input_data)
104
  elif input_data['search_type'] == 'position':
105
  self.open_view_targets(input_data)
@@ -111,44 +116,88 @@ class HomeWindowController:
111
 
112
  def open_view_targets(self, input_data):
113
  try:
114
- # Create find targets controller to use its model
115
- find_targets_controller = self.global_settings.get_find_targets_window()
116
-
117
- # Get targets using the model
118
- targets = find_targets_controller.model.find_targets(input_data)
119
-
120
- if targets:
121
- self.logger.debug(f"Found {len(targets)} targets")
122
-
123
- # Close existing View Targets tab if it exists
124
- main_window = self.global_settings.main_window
125
- existing_tab = main_window.find_tab_by_title("View Targets")
126
- if existing_tab:
127
- tab_index = main_window.view.tab_widget.indexOf(existing_tab)
128
- main_window._close_tab(tab_index)
129
- self.logger.debug("Closed existing View Targets tab")
130
-
131
- # Create view targets controller
132
- view_targets_controller = self.global_settings.get_view_targets_window()
133
-
134
- view_targets_controller.load_guides(
135
- targets, # Pass the targets directly
136
- input_data['organism'],
137
- input_data['endonuclease']
138
- )
139
 
140
- # Open new view targets tab
141
- main_window.open_new_tab(
142
- "View Targets",
143
- view_targets_controller
144
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- else:
147
- QMessageBox.warning(
148
- self.view,
149
- "No Targets Found",
150
- "No targets were found for the specified search."
151
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  except Exception as e:
154
  self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
@@ -157,22 +206,38 @@ class HomeWindowController:
157
  def open_find_targets_module(self):
158
  """Open find targets module for non-position searches"""
159
  try:
160
- # Close existing Find Targets tab if it exists
161
- main_window = self.global_settings.main_window
162
- existing_tab = main_window.find_tab_by_title("Find Targets")
163
- if existing_tab:
164
- tab_index = main_window.view.tab_widget.indexOf(existing_tab)
165
- main_window._close_tab(tab_index)
166
- self.logger.debug("Closed existing Find Targets tab")
167
-
168
- # Create new find targets controller and load data
169
- find_targets_controller = self.global_settings.get_find_targets_window()
170
- input_data = self.view.get_find_targets_input()
171
- find_targets_controller.find_targets(input_data)
172
-
173
- # Open new Find Targets tab
174
- self.global_settings.main_window.open_new_tab("Find Targets", find_targets_controller)
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  except Exception as e:
177
  show_error(self.global_settings, "Error in open_find_targets_module() in Home", str(e))
178
 
@@ -209,14 +274,29 @@ class HomeWindowController:
209
 
210
  def open_multitargeting_analysis_module(self):
211
  try:
 
 
 
212
  main_window = self.global_settings.main_window
213
  existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
 
 
 
 
214
  if existing_tab:
215
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
216
  main_window._resize_for_tab("Multitargeting Analysis")
 
217
  else:
 
218
  multitargeting_controller = self.global_settings.get_multitargeting_window()
 
 
 
219
  main_window.open_new_tab("Multitargeting Analysis", multitargeting_controller)
 
 
 
220
  except Exception as e:
221
  show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in Home", str(e))
222
 
@@ -267,9 +347,20 @@ class HomeWindowController:
267
 
268
  def _handle_db_validation_changed(self, is_valid, message):
269
  """Handle database validation state changes"""
270
- if not is_valid:
271
- self.view.show_warning("Database Warning", message)
272
- self._update_validation_state(is_valid)
 
 
 
 
 
 
 
 
 
 
 
273
 
274
  def _handle_db_state_changed(self, is_valid, message, changes):
275
  """Handle database state changes"""
@@ -309,18 +400,6 @@ class HomeWindowController:
309
  """Handle changes to the annotation file selection"""
310
  self.global_settings.set_current_annotation_file(new_file)
311
 
312
- def _update_cspr_related_ui(self):
313
- # Implementation to update UI elements that depend on CSPR files
314
- pass
315
-
316
- def _update_gbff_related_ui(self):
317
- # Implementation to update UI elements that depend on GBFF files
318
- pass
319
-
320
- def _update_validation_state(self, is_valid):
321
- # Implementation to update UI elements based on validation state
322
- pass
323
-
324
  def handle_search_type_change(self):
325
  """Update UI elements based on search type"""
326
  try:
 
1
+ from PyQt6 import QtWidgets
2
+ from PyQt6.QtWidgets import QMessageBox
 
3
  from views.HomeWindowView import HomeWindowView
4
  from models.HomeWindowModel import HomeWindowModel
5
+ from utils.ui import show_error
 
 
6
  from models.DatabaseManager import FileChangeType
7
+ import time
8
+ from views.LoadingDialog import LoadingDialog
9
+ from PyQt6.QtWidgets import QApplication
10
 
11
  class HomeWindowController:
12
  def __init__(self, global_settings):
 
72
  self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
73
 
74
  # grpStep3
 
 
75
  self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
76
  self.view.radio_button_position.clicked.connect(self.handle_search_type_change)
77
  self.view.radio_button_sequence.clicked.connect(self.handle_search_type_change)
 
98
  "The sequence given is too small. At least 100 characters are required."
99
  )
100
  return
101
+ if len(sequence) > 10000:
102
+ QMessageBox.warning(
103
+ self.view,
104
+ "Sequence Too Long",
105
+ "The sequence given is too large. Maximum allowed length is 10,000 base pairs."
106
+ )
107
+ return
108
  self.open_view_targets(input_data)
109
  elif input_data['search_type'] == 'position':
110
  self.open_view_targets(input_data)
 
116
 
117
  def open_view_targets(self, input_data):
118
  try:
119
+ # Create and show loading dialog
120
+ loading_dialog = LoadingDialog(self.view)
121
+ loading_dialog.show()
122
+ loading_dialog.set_progress(0)
123
+ QApplication.processEvents()
124
+
125
+ try:
126
+ # Create find targets controller to use its model
127
+ find_targets_controller = self.global_settings.get_find_targets_window()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ # For position searches, handle each query separately
130
+ if input_data['search_type'] == 'position':
131
+ queries = input_data['search_query'].strip().split('\n')
132
+ all_targets = []
133
+ total_queries = len(queries)
134
+
135
+ for i, query in enumerate(queries):
136
+ # Update loading progress for each query
137
+ progress = int((i / total_queries) * 80) # Leave room for final steps
138
+ loading_dialog.set_message(f"Processing position {i+1} of {total_queries}...", progress)
139
+ QApplication.processEvents()
140
+
141
+ # Create a copy of input data with single query
142
+ query_data = input_data.copy()
143
+ query_data['search_query'] = query.strip()
144
+
145
+ # Get targets for this query
146
+ targets = find_targets_controller.model.find_targets(query_data)
147
+ if targets:
148
+ # Add query information to each target
149
+ for target in targets:
150
+ target['original_query'] = query.strip()
151
+ all_targets.extend(targets)
152
+
153
+ targets = all_targets # Use combined results
154
+ self.logger.debug(f"Processed {len(queries)} queries, found total {len(targets)} targets")
155
+ else:
156
+ # For non-position searches, process normally
157
+ loading_dialog.set_message("Finding targets...", 20)
158
+ QApplication.processEvents()
159
+ targets = find_targets_controller.model.find_targets(input_data)
160
 
161
+ if targets:
162
+ self.logger.debug(f"Found {len(targets)} targets")
163
+ loading_dialog.set_message("Preparing view targets...", 80)
164
+ QApplication.processEvents()
165
+
166
+ # Close existing View Targets tab if it exists
167
+ main_window = self.global_settings.main_window
168
+ existing_tab = main_window.find_tab_by_title("View Targets")
169
+ if existing_tab:
170
+ tab_index = main_window.view.tab_widget.indexOf(existing_tab)
171
+ main_window._close_tab(tab_index)
172
+ self.logger.debug("Closed existing View Targets tab")
173
+
174
+ # Create view targets controller
175
+ loading_dialog.set_message("Creating view targets...", 90)
176
+ QApplication.processEvents()
177
+ view_targets_controller = self.global_settings.get_view_targets_window()
178
+
179
+ view_targets_controller.load_guides(
180
+ targets,
181
+ input_data['organism'],
182
+ input_data['endonuclease'],
183
+ loading_dialog=loading_dialog
184
+ )
185
+
186
+ # Open new view targets tab
187
+ main_window.open_new_tab(
188
+ "View Targets",
189
+ view_targets_controller
190
+ )
191
+
192
+ else:
193
+ QtWidgets.QMessageBox.warning(
194
+ self.view,
195
+ "No Targets Found",
196
+ "No targets were found for the specified search."
197
+ )
198
+
199
+ finally:
200
+ loading_dialog.close()
201
 
202
  except Exception as e:
203
  self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
 
206
  def open_find_targets_module(self):
207
  """Open find targets module for non-position searches"""
208
  try:
209
+ # Show loading dialog
210
+ loading_dialog = LoadingDialog(self.view)
211
+ loading_dialog.show()
212
+ loading_dialog.set_progress(0)
213
+ QApplication.processEvents()
 
 
 
 
 
 
 
 
 
 
214
 
215
+ try:
216
+ # Close existing Find Targets tab if it exists
217
+ main_window = self.global_settings.main_window
218
+ existing_tab = main_window.find_tab_by_title("Find Targets")
219
+ if existing_tab:
220
+ tab_index = main_window.view.tab_widget.indexOf(existing_tab)
221
+ main_window._close_tab(tab_index)
222
+ self.logger.debug("Closed existing Find Targets tab")
223
+
224
+ loading_dialog.set_progress(40)
225
+
226
+ # Create new find targets controller and load data
227
+ find_targets_controller = self.global_settings.get_find_targets_window()
228
+ input_data = self.view.get_find_targets_input()
229
+ loading_dialog.set_progress(60)
230
+
231
+ find_targets_controller.find_targets(input_data)
232
+ loading_dialog.set_progress(80)
233
+
234
+ # Open new Find Targets tab
235
+ self.global_settings.main_window.open_new_tab("Find Targets", find_targets_controller)
236
+ loading_dialog.set_progress(100)
237
+
238
+ finally:
239
+ loading_dialog.close()
240
+
241
  except Exception as e:
242
  show_error(self.global_settings, "Error in open_find_targets_module() in Home", str(e))
243
 
 
274
 
275
  def open_multitargeting_analysis_module(self):
276
  try:
277
+ start_time = time.time()
278
+ self.logger.debug("Starting multitargeting analysis module launch")
279
+
280
  main_window = self.global_settings.main_window
281
  existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
282
+
283
+ tab_check_time = time.time()
284
+ self.logger.debug(f"Tab check took: {tab_check_time - start_time:.2f} seconds")
285
+
286
  if existing_tab:
287
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
288
  main_window._resize_for_tab("Multitargeting Analysis")
289
+ self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
290
  else:
291
+ controller_start = time.time()
292
  multitargeting_controller = self.global_settings.get_multitargeting_window()
293
+ self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
294
+
295
+ tab_open_start = time.time()
296
  main_window.open_new_tab("Multitargeting Analysis", multitargeting_controller)
297
+ self.logger.debug(f"Tab opening took: {time.time() - tab_open_start:.2f} seconds")
298
+
299
+ self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
300
  except Exception as e:
301
  show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in Home", str(e))
302
 
 
347
 
348
  def _handle_db_validation_changed(self, is_valid, message):
349
  """Handle database validation state changes"""
350
+ try:
351
+ if not is_valid:
352
+ self.view.show_warning("Database Warning", message)
353
+
354
+ # Update UI elements based on validation state
355
+ self.view.push_button_find_view_targets.setEnabled(is_valid)
356
+ self.view.push_button_multitargeting_analysis.setEnabled(is_valid)
357
+ self.view.push_button_population_analysis.setEnabled(is_valid)
358
+
359
+ # Log the validation state change
360
+ self.logger.debug(f"Database validation state changed to: {is_valid}")
361
+
362
+ except Exception as e:
363
+ self.logger.error(f"Error handling database validation change: {str(e)}")
364
 
365
  def _handle_db_state_changed(self, is_valid, message, changes):
366
  """Handle database state changes"""
 
400
  """Handle changes to the annotation file selection"""
401
  self.global_settings.set_current_annotation_file(new_file)
402
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  def handle_search_type_change(self):
404
  """Update UI elements based on search type"""
405
  try:
src/controllers/MainWindowController.py CHANGED
@@ -41,11 +41,6 @@ class MainWindowController(LoggingMixin):
41
  self.view.action_open_NCBI_BLAST.triggered.connect(self._open_ncbi_blast_website)
42
  self.view.action_open_NCBI.triggered.connect(self._open_ncbi_website)
43
 
44
- # Title Bar
45
- self.view.close_window_button.clicked.connect(self._close_window)
46
- self.view.minimize_window_button.clicked.connect(self._minimize_window)
47
- self.view.maximize_window_button.clicked.connect(self._maximize_window)
48
-
49
  # Tab bar
50
  self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
51
  self.view.tab_widget.tabCloseRequested.connect(self._close_tab)
@@ -54,8 +49,8 @@ class MainWindowController(LoggingMixin):
54
  self.settings.first_time_startup.connect(self._handle_first_time_startup)
55
 
56
  # Add Button Menu
57
- self.view.action_new_genome.triggered.connect(self.open_new_genome_tab)
58
- self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
59
 
60
  # Settings Menu
61
  self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
@@ -94,18 +89,23 @@ class MainWindowController(LoggingMixin):
94
  def _switch_to_home_from_startup(self):
95
  self.log_method_call("_switch_to_home_from_startup")
96
 
 
 
 
 
 
 
97
  startup_tab = self.find_tab_by_title("Startup")
98
  if startup_tab:
99
  index = self.view.tab_widget.indexOf(startup_tab)
100
  self._close_tab(index)
101
-
102
- if self.startup_controller:
103
- self.startup_controller.deactivate()
104
- self.startup_controller = None
105
  else:
106
  self.log_warning("Startup tab not found when trying to close it")
107
 
108
- self.close_new_genome_and_switch_to_home()
 
 
 
109
  self._center_window()
110
 
111
  def _center_window(self):
@@ -179,18 +179,6 @@ class MainWindowController(LoggingMixin):
179
  def _open_ncbi_blast_website(self):
180
  ncbi_blast_page()
181
 
182
- def _close_window(self):
183
- self.view.close()
184
-
185
- def _minimize_window(self):
186
- self.view.showMinimized()
187
-
188
- def _maximize_window(self):
189
- if self.view.isMaximized():
190
- self.view.showNormal()
191
- else:
192
- self.view.showMaximized()
193
-
194
  def _on_tab_closed(self, widget):
195
  """
196
  Handle the tab_closed signal from CloseableTabWidget
@@ -259,9 +247,8 @@ class MainWindowController(LoggingMixin):
259
  def _resize_for_tab(self, title):
260
  try:
261
  if title == "Startup":
262
- # For Startup tab, set fixed size and disable maximize button
263
  self.view.setFixedSize(self.startup_size)
264
- self.view.setWindowFlags(self.view.windowFlags() & ~Qt.WindowType.WindowMaximizeButtonHint)
265
  elif title in ["View Targets", "Multitargeting Analysis"]:
266
  # Store current size before applying constraints
267
  if self.current_tab not in ["View Targets", "Multitargeting Analysis"]:
@@ -282,12 +269,10 @@ class MainWindowController(LoggingMixin):
282
  # Set minimum size constraints
283
  self.view.setMinimumSize(QSize(min_width, min_height))
284
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
285
- self.view.setWindowFlags(self.view.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
286
  else:
287
  # For all other tabs
288
  self.view.setMinimumSize(QSize(400, 300))
289
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
290
- self.view.setWindowFlags(self.view.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
291
 
292
  # Restore previous size if available and coming from View Targets or Multi-targeting Analysis
293
  if self.current_tab in ["View Targets", "Multitargeting Analysis"] and self.previous_size:
@@ -295,9 +280,6 @@ class MainWindowController(LoggingMixin):
295
  elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
296
  self.view.resize(self.shared_tab_size)
297
 
298
- # Ensure window flags are updated
299
- self.view.show()
300
-
301
  # Update the current tab
302
  self.current_tab = title
303
 
@@ -328,7 +310,7 @@ class MainWindowController(LoggingMixin):
328
  if title == "New Genome":
329
  home_tab = self.find_tab_by_title("Home")
330
  if home_tab:
331
- home_controller = self.global_settings.get_home_window()
332
  home_controller.refresh_data()
333
 
334
  # Resize for the current tab
@@ -367,7 +349,7 @@ class MainWindowController(LoggingMixin):
367
  self.view.tab_widget.setCurrentWidget(existing_tab)
368
  else:
369
  # If it doesn't exist, create a new one
370
- new_genome_controller = self.global_settings.get_new_genome_window()
371
  new_genome_view = new_genome_controller.view
372
  tab_index = self.view.tab_widget.addTab(new_genome_view, "New Genome")
373
  self.view.tab_widget.setCurrentIndex(tab_index)
 
41
  self.view.action_open_NCBI_BLAST.triggered.connect(self._open_ncbi_blast_website)
42
  self.view.action_open_NCBI.triggered.connect(self._open_ncbi_website)
43
 
 
 
 
 
 
44
  # Tab bar
45
  self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
46
  self.view.tab_widget.tabCloseRequested.connect(self._close_tab)
 
49
  self.settings.first_time_startup.connect(self._handle_first_time_startup)
50
 
51
  # Add Button Menu
52
+ # self.view.action_new_genome.triggered.connect(self.open_new_genome_tab)
53
+ # self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
54
 
55
  # Settings Menu
56
  self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
 
89
  def _switch_to_home_from_startup(self):
90
  self.log_method_call("_switch_to_home_from_startup")
91
 
92
+ # First deactivate startup controller
93
+ if self.startup_controller:
94
+ self.startup_controller.deactivate()
95
+ self.startup_controller = None
96
+
97
+ # Close startup tab if it exists
98
  startup_tab = self.find_tab_by_title("Startup")
99
  if startup_tab:
100
  index = self.view.tab_widget.indexOf(startup_tab)
101
  self._close_tab(index)
 
 
 
 
102
  else:
103
  self.log_warning("Startup tab not found when trying to close it")
104
 
105
+ # Open home tab and ensure it's properly initialized
106
+ self._open_home_tab()
107
+
108
+ # Center the window after all tab operations
109
  self._center_window()
110
 
111
  def _center_window(self):
 
179
  def _open_ncbi_blast_website(self):
180
  ncbi_blast_page()
181
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def _on_tab_closed(self, widget):
183
  """
184
  Handle the tab_closed signal from CloseableTabWidget
 
247
  def _resize_for_tab(self, title):
248
  try:
249
  if title == "Startup":
250
+ # For Startup tab, set fixed size but keep window controls
251
  self.view.setFixedSize(self.startup_size)
 
252
  elif title in ["View Targets", "Multitargeting Analysis"]:
253
  # Store current size before applying constraints
254
  if self.current_tab not in ["View Targets", "Multitargeting Analysis"]:
 
269
  # Set minimum size constraints
270
  self.view.setMinimumSize(QSize(min_width, min_height))
271
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
 
272
  else:
273
  # For all other tabs
274
  self.view.setMinimumSize(QSize(400, 300))
275
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
 
276
 
277
  # Restore previous size if available and coming from View Targets or Multi-targeting Analysis
278
  if self.current_tab in ["View Targets", "Multitargeting Analysis"] and self.previous_size:
 
280
  elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
281
  self.view.resize(self.shared_tab_size)
282
 
 
 
 
283
  # Update the current tab
284
  self.current_tab = title
285
 
 
310
  if title == "New Genome":
311
  home_tab = self.find_tab_by_title("Home")
312
  if home_tab:
313
+ home_controller = self.settings.get_home_window()
314
  home_controller.refresh_data()
315
 
316
  # Resize for the current tab
 
349
  self.view.tab_widget.setCurrentWidget(existing_tab)
350
  else:
351
  # If it doesn't exist, create a new one
352
+ new_genome_controller = self.settings.get_new_genome_window()
353
  new_genome_view = new_genome_controller.view
354
  tab_index = self.view.tab_widget.addTab(new_genome_view, "New Genome")
355
  self.view.tab_widget.setCurrentIndex(tab_index)
src/controllers/MultitargetingWindowController.py CHANGED
@@ -2,18 +2,17 @@ from PyQt6.QtWidgets import QMainWindow
2
  from views.MultitargetingWindowView import MultitargetingWindowView
3
  from models.MultitargetingWindowModel import MultitargetingWindowModel
4
  from utils.ui import show_error, show_message
 
5
 
6
  class MultitargetingWindowController(QMainWindow):
7
  def __init__(self, global_settings):
8
  super().__init__()
9
  self.settings = global_settings
10
  self.logger = global_settings.get_logger()
11
-
12
  try:
13
  self._model = MultitargetingWindowModel(global_settings)
14
  self._view = MultitargetingWindowView(global_settings)
15
  self.setCentralWidget(self._view)
16
-
17
  self._init_ui()
18
  self._setup_connections()
19
  except Exception as e:
@@ -32,33 +31,24 @@ class MultitargetingWindowController(QMainWindow):
32
  if organisms:
33
  self._on_organism_changed(0)
34
 
35
- # Initialize plots
36
- self._view.setup_plots()
37
-
38
- # Connect max results line edit
39
- self._view.line_edit_max_results.textChanged.connect(self._on_max_results_changed)
40
-
41
  except Exception as e:
42
  self.logger.error(f"Error in _init_ui: {str(e)}")
43
  show_error(self.settings, "Error", f"Failed to initialize UI: {str(e)}")
44
 
45
  def _setup_connections(self):
46
  """Set up signal-slot connections"""
47
- # Organism and endonuclease selection
48
  self._view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
49
  self._view.combo_box_endonuclease.currentIndexChanged.connect(self._on_endonuclease_changed)
50
 
51
- # Buttons
52
  self._view.push_button_analyze.clicked.connect(self._on_analyze_clicked)
53
- # self._view.push_button_statistics_overview.clicked.connect(self._on_statistics_overview_clicked)
54
- # self._view.tool_button_sql_settings.clicked.connect(self._on_sql_settings_clicked)
55
 
56
- # Table selection
57
  self._view.table_seeds.itemSelectionChanged.connect(self._on_seed_selected)
58
  self._view.check_box_select_all.stateChanged.connect(self._on_select_all_changed)
59
 
60
  self._view.push_button_export_selected_gRNAs.clicked.connect(self._handle_export)
61
 
 
 
62
  def _on_organism_changed(self, index):
63
  """Handle organism selection change"""
64
  try:
@@ -87,7 +77,14 @@ class MultitargetingWindowController(QMainWindow):
87
 
88
  def _on_analyze_clicked(self):
89
  """Handle analyze button click"""
 
90
  try:
 
 
 
 
 
 
91
  organism = self._view.combo_box_organism.currentText()
92
  endo = self._view.combo_box_endonuclease.currentText()
93
 
@@ -98,6 +95,13 @@ class MultitargetingWindowController(QMainWindow):
98
  # Load data
99
  try:
100
  self._model.set_files(organism, endo)
 
 
 
 
 
 
 
101
  except FileNotFoundError as e:
102
  show_error(self.settings, "File Error",
103
  f"Could not find required files for {organism} with {endo}. Please ensure the files exist.")
@@ -106,33 +110,11 @@ class MultitargetingWindowController(QMainWindow):
106
  show_error(self.settings, "Input Error", str(e))
107
  return
108
 
109
- seeds_data = self._model.get_repeats_data()
110
-
111
- # Update UI
112
- self._view.update_seeds_table(seeds_data)
113
- self._update_plots()
114
 
115
  except Exception as e:
116
  show_error(self.settings, "Analysis Error", str(e))
117
 
118
- def _on_statistics_overview_clicked(self):
119
- """Handle statistics overview button click"""
120
- try:
121
- stats = self._model.calculate_statistics()
122
- self._show_statistics_dialog(stats)
123
- except Exception as e:
124
- show_error(self.settings, "Statistics Error", str(e))
125
-
126
- def _on_sql_settings_clicked(self):
127
- """Handle SQL settings button click"""
128
- try:
129
- current_settings = self._model.get_sql_settings()
130
- if self._show_sql_settings_dialog(current_settings):
131
- new_settings = self._get_sql_settings_from_dialog()
132
- self._model.update_sql_settings(new_settings)
133
- except Exception as e:
134
- show_error(self.settings, "SQL Settings Error", str(e))
135
-
136
  def _on_seed_selected(self):
137
  """Handle seed selection in table"""
138
  try:
@@ -253,32 +235,21 @@ class MultitargetingWindowController(QMainWindow):
253
  self.logger.error(f"Error in _update_plots: {str(e)}")
254
  show_error(self.settings, "Plot Update Error", str(e))
255
 
256
- def _show_statistics_dialog(self, stats):
257
- """Show statistics overview dialog"""
258
- # Implement statistics dialog display
259
- pass
260
-
261
- def _show_sql_settings_dialog(self, current_settings):
262
- """Show SQL settings dialog"""
263
- # Implement SQL settings dialog display
264
- return False
265
-
266
- def _get_sql_settings_from_dialog(self):
267
- """Get settings from SQL settings dialog"""
268
- # Implement getting settings from dialog
269
- return {}
270
-
271
  def _on_max_results_changed(self, value):
272
  """Handle changes to max results setting"""
273
  try:
274
- if value == "": # Handle empty input
275
  self._model.set_row_limit(1000) # Reset to default
276
  return
277
 
278
- # Convert to int and update model
279
  limit = int(value)
280
- if limit <= 0: # Handle negative or zero values
281
- limit = -1 # Use -1 to indicate no limit
 
 
 
 
282
  self._model.set_row_limit(limit)
283
 
284
  except ValueError:
 
2
  from views.MultitargetingWindowView import MultitargetingWindowView
3
  from models.MultitargetingWindowModel import MultitargetingWindowModel
4
  from utils.ui import show_error, show_message
5
+ import time
6
 
7
  class MultitargetingWindowController(QMainWindow):
8
  def __init__(self, global_settings):
9
  super().__init__()
10
  self.settings = global_settings
11
  self.logger = global_settings.get_logger()
 
12
  try:
13
  self._model = MultitargetingWindowModel(global_settings)
14
  self._view = MultitargetingWindowView(global_settings)
15
  self.setCentralWidget(self._view)
 
16
  self._init_ui()
17
  self._setup_connections()
18
  except Exception as e:
 
31
  if organisms:
32
  self._on_organism_changed(0)
33
 
 
 
 
 
 
 
34
  except Exception as e:
35
  self.logger.error(f"Error in _init_ui: {str(e)}")
36
  show_error(self.settings, "Error", f"Failed to initialize UI: {str(e)}")
37
 
38
  def _setup_connections(self):
39
  """Set up signal-slot connections"""
 
40
  self._view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
41
  self._view.combo_box_endonuclease.currentIndexChanged.connect(self._on_endonuclease_changed)
42
 
 
43
  self._view.push_button_analyze.clicked.connect(self._on_analyze_clicked)
 
 
44
 
 
45
  self._view.table_seeds.itemSelectionChanged.connect(self._on_seed_selected)
46
  self._view.check_box_select_all.stateChanged.connect(self._on_select_all_changed)
47
 
48
  self._view.push_button_export_selected_gRNAs.clicked.connect(self._handle_export)
49
 
50
+ self._view.line_edit_max_results.textChanged.connect(self._on_max_results_changed)
51
+
52
  def _on_organism_changed(self, index):
53
  """Handle organism selection change"""
54
  try:
 
77
 
78
  def _on_analyze_clicked(self):
79
  """Handle analyze button click"""
80
+ analyze_start = time.time()
81
  try:
82
+ # Initialize plots if not already done
83
+ if not hasattr(self._view, 'repeats_vs_seed_canvas'):
84
+ plot_init_start = time.time()
85
+ self._view.setup_plots()
86
+ self.logger.debug(f"Plot initialization took: {time.time() - plot_init_start:.2f} seconds")
87
+
88
  organism = self._view.combo_box_organism.currentText()
89
  endo = self._view.combo_box_endonuclease.currentText()
90
 
 
95
  # Load data
96
  try:
97
  self._model.set_files(organism, endo)
98
+
99
+ seeds_data = self._model.get_repeats_data()
100
+
101
+ self._view.update_seeds_table(seeds_data)
102
+
103
+ self._update_plots()
104
+
105
  except FileNotFoundError as e:
106
  show_error(self.settings, "File Error",
107
  f"Could not find required files for {organism} with {endo}. Please ensure the files exist.")
 
110
  show_error(self.settings, "Input Error", str(e))
111
  return
112
 
113
+ self.logger.debug(f"Total analysis took: {time.time() - analyze_start:.2f} seconds")
 
 
 
 
114
 
115
  except Exception as e:
116
  show_error(self.settings, "Analysis Error", str(e))
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  def _on_seed_selected(self):
119
  """Handle seed selection in table"""
120
  try:
 
235
  self.logger.error(f"Error in _update_plots: {str(e)}")
236
  show_error(self.settings, "Plot Update Error", str(e))
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  def _on_max_results_changed(self, value):
239
  """Handle changes to max results setting"""
240
  try:
241
+ if not value: # Handle empty input
242
  self._model.set_row_limit(1000) # Reset to default
243
  return
244
 
245
+ # Convert to int and validate
246
  limit = int(value)
247
+ if limit <= 0: # Handle negative values
248
+ self._model.set_row_limit(1000) # Reset to default
249
+ self._view.line_edit_max_results.setText("1000")
250
+ return
251
+
252
+ # Update model
253
  self._model.set_row_limit(limit)
254
 
255
  except ValueError:
src/controllers/NCBIWindowController.py CHANGED
@@ -32,8 +32,6 @@ class NCBIWindowController:
32
  self.view.push_button_download_files.clicked.connect(self.download_files_wrapper)
33
  self.view.check_box_select_all_rows.clicked.connect(self.select_all_rows_in_table)
34
  self.view.radio_button_collections_genbank.toggled.connect(self.is_checked_GenBank_radio_button)
35
-
36
- self.logger.debug("NCBI Window connections setup completed")
37
  except Exception as e:
38
  self.logger.error(f"Error setting up connections: {str(e)}", exc_info=True)
39
  show_error(self.settings, "Error setting up connections", str(e))
@@ -48,20 +46,25 @@ class NCBIWindowController:
48
  self.view.reset_progress()
49
  search_params = self.view.get_search_parameters()
50
 
51
- # Set default values if fields are empty
 
 
 
52
  if not search_params['organism'].strip():
53
  search_params['organism'] = "Escherichia coli"
 
 
54
  if not search_params['strain'].strip():
55
  search_params['strain'] = "K-12"
 
56
 
57
- self.logger.info(f"Querying NCBI with parameters: {search_params}")
58
-
59
  self.df = self.model.search_ncbi(search_params)
60
 
61
  if self.df.empty:
62
  print("No results found")
63
  self.logger.warning("No results found for the given search parameters.")
64
- show_message(12, QtWidgets.QMessageBox.Icon.Warning, "No Results", "No results found for the given search parameters.")
 
65
  else:
66
  print(f"Query returned {len(self.df)} results")
67
  self.logger.info(f"Query returned {len(self.df)} results")
@@ -69,7 +72,6 @@ class NCBIWindowController:
69
 
70
  self.view.activateWindow()
71
  except Exception as e:
72
- print(f"Error in query_db: {str(e)}")
73
  self.logger.error(f"Error in query_db: {str(e)}", exc_info=True)
74
  show_error(self.settings, "Error in query_db", e)
75
 
@@ -117,7 +119,7 @@ class NCBIWindowController:
117
  try:
118
  self.view.reset_progress()
119
  self.total_files = len(selected_rows)
120
- self.view.progress_bar_download_files.setMaximum(self.total_files)
121
  self.view.set_download_files_status_label("Preparing to download...")
122
 
123
  self.model.clear_downloaded_files()
@@ -131,30 +133,36 @@ class NCBIWindowController:
131
  strain = self.proxy_model.data(self.proxy_model.index(index.row(), 2))
132
  self.logger.info(f"Processing ID: {id}")
133
 
134
- url = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
135
- self.logger.info(f"Download URL for ID {id}: {url}")
136
- if not url:
 
137
  self.logger.warning(f"No download URL found for ID: {id}")
138
  self.unavailable_files.append((species_name, strain))
139
  self.on_thread_completed() # Increment completed threads for unavailable files
140
  continue
141
 
142
- downloader = self.model.DownloadThread(self, url, id, species_name, strain,
143
- self.view.check_box_file_types_fna.isChecked(),
144
- self.view.check_box_file_types_gbff.isChecked())
145
- downloader.finished.connect(self.on_download_finished)
146
- downloader.progress_updated.connect(self.update_progress)
147
- downloader.status_updated.connect(self.update_status)
148
- downloader.all_completed.connect(self.on_thread_completed)
149
- self.download_threads.append(downloader)
150
- downloader.start()
 
 
 
 
 
151
 
152
  if not self.download_threads and not self.unavailable_files:
153
  show_message(12, QtWidgets.QMessageBox.Icon.Information, "No Downloads", "No valid files selected for download.")
154
  return
155
 
156
  except Exception as e:
157
- self.logger.error(f"Error in download_files: {str(e)}", exc_info=True)
158
  show_error(self.settings, "Error in download_files", e)
159
 
160
  def on_thread_completed(self):
 
32
  self.view.push_button_download_files.clicked.connect(self.download_files_wrapper)
33
  self.view.check_box_select_all_rows.clicked.connect(self.select_all_rows_in_table)
34
  self.view.radio_button_collections_genbank.toggled.connect(self.is_checked_GenBank_radio_button)
 
 
35
  except Exception as e:
36
  self.logger.error(f"Error setting up connections: {str(e)}", exc_info=True)
37
  show_error(self.settings, "Error setting up connections", str(e))
 
46
  self.view.reset_progress()
47
  search_params = self.view.get_search_parameters()
48
 
49
+ # Set the current database in the model
50
+ self.model.current_database = search_params['database']
51
+
52
+ # Check if organism and strain are provided, set defaults if empty
53
  if not search_params['organism'].strip():
54
  search_params['organism'] = "Escherichia coli"
55
+ self.view.line_edit_organism.setText(search_params['organism'])
56
+
57
  if not search_params['strain'].strip():
58
  search_params['strain'] = "K-12"
59
+ self.view.line_edit_strain.setText(search_params['strain'])
60
 
 
 
61
  self.df = self.model.search_ncbi(search_params)
62
 
63
  if self.df.empty:
64
  print("No results found")
65
  self.logger.warning("No results found for the given search parameters.")
66
+ show_message(12, QtWidgets.QMessageBox.Icon.Warning, "No Results",
67
+ "No results found for the given search parameters.")
68
  else:
69
  print(f"Query returned {len(self.df)} results")
70
  self.logger.info(f"Query returned {len(self.df)} results")
 
72
 
73
  self.view.activateWindow()
74
  except Exception as e:
 
75
  self.logger.error(f"Error in query_db: {str(e)}", exc_info=True)
76
  show_error(self.settings, "Error in query_db", e)
77
 
 
119
  try:
120
  self.view.reset_progress()
121
  self.total_files = len(selected_rows)
122
+ self.view.progress_bar_download_files.setMaximum(self.total_files * 2) # *2 for potential FNA and GBFF files
123
  self.view.set_download_files_status_label("Preparing to download...")
124
 
125
  self.model.clear_downloaded_files()
 
133
  strain = self.proxy_model.data(self.proxy_model.index(index.row(), 2))
134
  self.logger.info(f"Processing ID: {id}")
135
 
136
+ urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
137
+ self.logger.info(f"Download URLs for ID {id}: {urls}")
138
+
139
+ if not urls:
140
  self.logger.warning(f"No download URL found for ID: {id}")
141
  self.unavailable_files.append((species_name, strain))
142
  self.on_thread_completed() # Increment completed threads for unavailable files
143
  continue
144
 
145
+ # Create a download thread for each URL
146
+ for url in urls:
147
+ is_fna = '.fna.' in url.lower()
148
+ downloader = self.model.DownloadThread(
149
+ self, url, id, species_name, strain,
150
+ download_fna=is_fna,
151
+ download_gbff=not is_fna
152
+ )
153
+ downloader.finished.connect(self.on_download_finished)
154
+ downloader.progress_updated.connect(self.update_progress)
155
+ downloader.status_updated.connect(self.update_status)
156
+ downloader.all_completed.connect(self.on_thread_completed)
157
+ self.download_threads.append(downloader)
158
+ downloader.start()
159
 
160
  if not self.download_threads and not self.unavailable_files:
161
  show_message(12, QtWidgets.QMessageBox.Icon.Information, "No Downloads", "No valid files selected for download.")
162
  return
163
 
164
  except Exception as e:
165
+ self.logger.error(f"Error in download_files: {str(e)}")
166
  show_error(self.settings, "Error in download_files", e)
167
 
168
  def on_thread_completed(self):
src/controllers/NewGenomeWindowController.py CHANGED
@@ -2,6 +2,7 @@ from PyQt6 import QtWidgets, QtCore
2
  from models.NewGenomeWindowModel import NewGenomeWindowModel
3
  from views.NewGenomeWindowView import NewGenomeWindowView
4
  from utils.ui import show_message, show_error
 
5
 
6
  class NewGenomeWindowController:
7
  def __init__(self, global_settings):
@@ -198,9 +199,30 @@ class NewGenomeWindowController:
198
  program = self.model.get_job_command()
199
  command_args = self.model.get_arguments_command_for_job(row_index)
200
  self.logger.debug(f"Executing command: {program} {' '.join(command_args)}")
 
201
 
202
  if self.job_process.state() == QtCore.QProcess.ProcessState.NotRunning:
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  self.job_process.start(program, command_args)
 
 
 
 
 
 
 
204
  self.logger.debug("Job started")
205
  else:
206
  self.logger.warning("Process is still running, cannot start a new job.")
@@ -213,11 +235,27 @@ class NewGenomeWindowController:
213
  self.view.table_widget_jobs.viewport().update()
214
 
215
  def _handle_job_completion(self, exit_code=None, exit_status=None):
216
- self.logger.debug("Process finished")
 
 
 
 
 
 
 
 
 
217
 
218
  if hasattr(self, 'job_indexes') and self.job_indexes:
219
  completed_row_index = self.job_indexes.pop(0)
220
 
 
 
 
 
 
 
 
221
  # Set job as completed
222
  self.view.set_job_completed(completed_row_index)
223
 
@@ -246,8 +284,26 @@ class NewGenomeWindowController:
246
 
247
  def _open_ncbi_module(self):
248
  try:
 
 
 
 
 
249
  ncbi_controller = self.settings.get_ncbi_window()
 
 
 
 
 
 
 
 
 
 
 
 
250
  self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
 
251
  except Exception as e:
252
  show_error(self.settings, "Error opening NCBI module", str(e))
253
  self.logger.error(f"Failed to open NCBI module: {str(e)}")
 
2
  from models.NewGenomeWindowModel import NewGenomeWindowModel
3
  from views.NewGenomeWindowView import NewGenomeWindowView
4
  from utils.ui import show_message, show_error
5
+ import os
6
 
7
  class NewGenomeWindowController:
8
  def __init__(self, global_settings):
 
199
  program = self.model.get_job_command()
200
  command_args = self.model.get_arguments_command_for_job(row_index)
201
  self.logger.debug(f"Executing command: {program} {' '.join(command_args)}")
202
+ self.logger.debug(f"Working directory: {os.getcwd()}")
203
 
204
  if self.job_process.state() == QtCore.QProcess.ProcessState.NotRunning:
205
+ # Set up process output handling
206
+ def handle_stdout():
207
+ output = self.job_process.readAllStandardOutput().data().decode()
208
+ self.logger.debug(f"Process stdout: {output}")
209
+
210
+ def handle_stderr():
211
+ error = self.job_process.readAllStandardError().data().decode()
212
+ self.logger.error(f"Process stderr: {error}")
213
+
214
+ self.job_process.readyReadStandardOutput.connect(handle_stdout)
215
+ self.job_process.readyReadStandardError.connect(handle_stderr)
216
+
217
+ # Start the process
218
  self.job_process.start(program, command_args)
219
+
220
+ # Check if process started successfully
221
+ if not self.job_process.waitForStarted(3000): # 3 second timeout
222
+ self.logger.error("Process failed to start")
223
+ self.logger.error(f"Process error: {self.job_process.errorString()}")
224
+ return
225
+
226
  self.logger.debug("Job started")
227
  else:
228
  self.logger.warning("Process is still running, cannot start a new job.")
 
235
  self.view.table_widget_jobs.viewport().update()
236
 
237
  def _handle_job_completion(self, exit_code=None, exit_status=None):
238
+ self.logger.debug(f"Process finished with exit code: {exit_code}")
239
+
240
+ # Log any remaining output
241
+ remaining_output = self.job_process.readAllStandardOutput().data().decode()
242
+ if remaining_output:
243
+ self.logger.debug(f"Final process output: {remaining_output}")
244
+
245
+ remaining_error = self.job_process.readAllStandardError().data().decode()
246
+ if remaining_error:
247
+ self.logger.error(f"Final process error output: {remaining_error}")
248
 
249
  if hasattr(self, 'job_indexes') and self.job_indexes:
250
  completed_row_index = self.job_indexes.pop(0)
251
 
252
+ # Check if output files were created
253
+ expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{self.model.get_job_name(completed_row_index)}.cspr")
254
+ if os.path.exists(expected_cspr_file):
255
+ self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
256
+ else:
257
+ self.logger.error(f"Expected CSPR file not found: {expected_cspr_file}")
258
+
259
  # Set job as completed
260
  self.view.set_job_completed(completed_row_index)
261
 
 
284
 
285
  def _open_ncbi_module(self):
286
  try:
287
+ # Get organism and strain values from the view
288
+ organism_name = self.view.get_organism_name()
289
+ strain_name = self.view.get_strain()
290
+
291
+ # Get NCBI controller
292
  ncbi_controller = self.settings.get_ncbi_window()
293
+
294
+ # Connect to the initialization complete signal
295
+ def on_init_complete():
296
+ if organism_name:
297
+ ncbi_controller.view.line_edit_organism.setText(organism_name)
298
+ if strain_name:
299
+ ncbi_controller.view.line_edit_strain.setText(strain_name)
300
+
301
+ # Connect the signal
302
+ ncbi_controller.view.initialization_complete.connect(on_init_complete)
303
+
304
+ # Open the NCBI tab
305
  self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
306
+
307
  except Exception as e:
308
  show_error(self.settings, "Error opening NCBI module", str(e))
309
  self.logger.error(f"Failed to open NCBI module: {str(e)}")
src/controllers/StartupWindowController.py CHANGED
@@ -30,6 +30,7 @@ class StartupWindowController:
30
  self.view.push_button_go_to_home_or_new_genome.clicked.connect(self._handle_go_to_home_or_new_genome)
31
  self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
32
  self.model.db_state_updated.connect(self._on_db_state_updated)
 
33
  self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
34
 
35
  def _on_db_path_text_changed(self, new_path):
@@ -39,6 +40,11 @@ class StartupWindowController:
39
  if self.is_active and hasattr(self, 'view'):
40
  self.view.set_db_status(is_valid, message)
41
 
 
 
 
 
 
42
  def _init_ui(self):
43
  db_path = self.model.get_db_path()
44
  self.logger.debug(f"Initial database path: {db_path}")
@@ -74,13 +80,11 @@ class StartupWindowController:
74
  self.open_new_genome_tab()
75
 
76
  def restart_application(self):
77
- """Restart the entire application"""
78
  try:
79
  self.logger.info("Restarting application...")
80
  # Get the current application instance
81
  app = QtWidgets.QApplication.instance()
82
- # Use a custom exit code for restart (e.g., 1000)
83
- app.exit(1000) # Changed from QApplication.Exit.ExitCode.Restart
84
  except Exception as e:
85
  self.logger.error(f"Error restarting application: {str(e)}", exc_info=True)
86
  show_error(self.settings, "Error restarting application", str(e))
@@ -88,7 +92,8 @@ class StartupWindowController:
88
  def open_new_genome_tab(self):
89
  try:
90
  self.logger.debug("Opening New Genome tab")
91
- self.settings.main_window.open_new_genome_tab()
 
92
  except Exception as e:
93
  self.logger.error(f"Error opening New Genome tab: {str(e)}", exc_info=True)
94
  show_error(self.settings, "Error opening New Genome module", str(e))
 
30
  self.view.push_button_go_to_home_or_new_genome.clicked.connect(self._handle_go_to_home_or_new_genome)
31
  self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
32
  self.model.db_state_updated.connect(self._on_db_state_updated)
33
+ self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
34
  self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
35
 
36
  def _on_db_path_text_changed(self, new_path):
 
40
  if self.is_active and hasattr(self, 'view'):
41
  self.view.set_db_status(is_valid, message)
42
 
43
+ def _on_db_validation_changed(self, is_valid, message):
44
+ """Handle database validation state changes"""
45
+ if self.is_active and hasattr(self, 'view'):
46
+ self.view.set_db_status(is_valid, message)
47
+
48
  def _init_ui(self):
49
  db_path = self.model.get_db_path()
50
  self.logger.debug(f"Initial database path: {db_path}")
 
80
  self.open_new_genome_tab()
81
 
82
  def restart_application(self):
 
83
  try:
84
  self.logger.info("Restarting application...")
85
  # Get the current application instance
86
  app = QtWidgets.QApplication.instance()
87
+ app.exit(1000)
 
88
  except Exception as e:
89
  self.logger.error(f"Error restarting application: {str(e)}", exc_info=True)
90
  show_error(self.settings, "Error restarting application", str(e))
 
92
  def open_new_genome_tab(self):
93
  try:
94
  self.logger.debug("Opening New Genome tab")
95
+ if hasattr(self.settings, 'main_window'):
96
+ self.settings.main_window.open_new_genome_tab()
97
  except Exception as e:
98
  self.logger.error(f"Error opening New Genome tab: {str(e)}", exc_info=True)
99
  show_error(self.settings, "Error opening New Genome module", str(e))
src/controllers/ViewTargetsController.py CHANGED
@@ -1,15 +1,13 @@
1
- import logging
2
  from controllers.ScoringOptionsController import ScoringOptionsController
3
  from models.ViewTargetsModel import ViewTargetsModel
4
  from views.ViewTargetsView import ViewTargetsView
5
  from PyQt6.QtWidgets import QMessageBox
6
  from utils.ui import show_error
7
- import time
8
- from PyQt6 import QtWidgets, QtCore
9
  import traceback
10
- import threading
11
- from Bio.Seq import Seq
12
- import os
13
 
14
  class ViewTargetsController:
15
  def __init__(self, global_settings):
@@ -38,75 +36,161 @@ class ViewTargetsController:
38
 
39
  self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
40
  self.view.spin_box_minimum_on_target_score.valueChanged.connect(self.refresh_guides_display)
 
41
 
42
- def load_guides(self, selected_targets, organism, endonuclease):
 
 
 
 
 
 
 
 
 
 
43
  try:
44
  self.organism = organism
45
  self.endonuclease = endonuclease
46
  self.selected_targets = selected_targets
47
 
48
- print(f"Loading guides for {organism} and {selected_targets} with {endonuclease}")
49
-
50
- self.model.load_guides(selected_targets, organism, endonuclease)
 
 
 
51
 
52
- # Get available endonucleases for this organism
53
- org_to_endo = self.settings.get_organism_to_endonuclease()
54
- if organism in org_to_endo:
55
- available_endos = org_to_endo[organism]
56
- self.view.combo_box_endonuclease.clear()
57
- self.view.combo_box_endonuclease.addItems(available_endos)
 
 
 
58
 
59
- # Set current endonuclease
60
- current_index = self.view.combo_box_endonuclease.findText(endonuclease)
61
- if current_index >= 0:
62
- self.view.combo_box_endonuclease.setCurrentIndex(current_index)
 
63
 
64
- self.view.combo_box_endonuclease.currentTextChanged.connect(self._on_endonuclease_changed)
65
-
66
- # Format gene names for display
67
- seen_positions = set()
68
- formatted_genes = []
69
-
70
- if selected_targets and selected_targets[0].get('feature_type') == 'Position':
71
- position_groups = {}
72
- for target in selected_targets:
73
- position_name = target['feature_id']
74
- if position_name not in position_groups:
75
- position_groups[position_name] = target
76
- formatted_genes.append(position_name)
77
 
78
- if formatted_genes:
79
- first_guide = position_groups[formatted_genes[0]]
80
- self.view.line_edit_start_location.setText(str(first_guide['start']))
81
- self.view.line_edit_stop_location.setText(str(first_guide['end']))
 
 
 
 
 
82
 
83
- if 'gene_sequence' in first_guide:
84
- self.view.set_text_edit_gene_viewer(first_guide['gene_sequence'])
85
- else:
86
- for target in selected_targets:
87
- gene_name = target.get('feature_name')
88
- feature_id = target.get('feature_id')
 
 
 
 
 
 
 
 
 
89
 
90
- if gene_name and feature_id and gene_name not in seen_positions:
91
- seen_positions.add(gene_name)
92
- formatted_genes.append(f"{feature_id}: {gene_name}")
93
-
94
- formatted_genes.sort()
95
- self.view.set_combo_box_gene(formatted_genes)
96
-
97
- guides = self.model.get_guides()
98
- self.view.display_guides_in_table(guides)
99
-
100
- # Trigger gene sequence retrieval for first entry
101
- if formatted_genes:
102
- first_gene = formatted_genes[0]
103
- self.on_gene_selected(first_gene)
104
-
 
 
 
 
 
 
 
 
 
105
  except Exception as e:
106
  self.logger.error(f"Error in load_guides: {str(e)}")
107
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
108
  show_error(self.settings, "Error loading guides", str(e))
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  def _on_endonuclease_changed(self, new_endonuclease):
111
  try:
112
  if new_endonuclease != self.endonuclease:
@@ -142,36 +226,35 @@ class ViewTargetsController:
142
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
143
  show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
144
 
145
- def load_gene_viewer(self):
146
- try:
 
 
 
 
 
147
 
148
- # Get selected gene from combo box
149
- selected_text = self.view.combo_box_gene.currentText()
150
- if not selected_text:
151
- self.logger.debug("No gene selected")
152
- return
153
 
154
- # Extract locus tag from "locus_tag: gene_name" format
155
- locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
156
- self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
157
 
158
- # Get gene sequence with padding
159
- sequence_data = self.model.get_gene_sequence(locus_tag)
160
-
161
- if sequence_data:
162
- # Update gene viewer with sequence
163
- self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
164
 
165
- # Update location fields
166
- self.view.line_edit_start_location.setText(str(sequence_data['start']))
167
- self.view.line_edit_stop_location.setText(str(sequence_data['end']))
168
 
169
- else:
170
- self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
171
 
172
- except Exception as e:
173
- self.logger.error(f"Error in load_gene_viewer: {str(e)}")
174
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
175
 
176
  def perform_off_target_analysis(self):
177
  """Launch off-target analysis for selected guides"""
@@ -216,9 +299,8 @@ class ViewTargetsController:
216
  def _handle_off_target_results(self, results):
217
  """Handle off-target analysis results"""
218
  try:
219
- scores, details = results # Unpack the tuple of results
220
 
221
- # Get current table headers
222
  headers = self.view.get_table_headers()
223
 
224
  # Find Score column index
@@ -285,44 +367,33 @@ class ViewTargetsController:
285
  "Please select guides to highlight in the gene viewer.")
286
  return
287
 
288
- # Convert table selections to the format expected by the model
289
- guides_to_highlight = []
290
- for guide in selected_rows:
291
- guide_info = {
292
- 'location': guide['location'],
293
- 'sequence': guide['sequence'],
294
- 'strand': guide['strand']
295
- }
296
- guides_to_highlight.append(guide_info)
297
- self.logger.debug(f"Guide to highlight: {guide_info}")
298
-
299
- # Get current gene sequence
300
  current_gene = self.view.combo_box_gene.currentText()
 
301
 
302
  # Check if this is a position-based search
303
- if "chrom" in current_gene and "start:" in current_gene:
304
- # Parse position from the text (format: "chrom X, start: Y, end: Z")
305
  try:
306
  parts = current_gene.split(',')
307
- chrom = int(parts[0].split('chrom')[1].strip())
308
  start = int(parts[1].split('start:')[1].strip())
309
  end = int(parts[2].split('end:')[1].strip())
310
 
311
- # Get sequence directly from model's _get_sequence_for_position
312
  sequence = self.model._get_sequence_for_position(chrom, start, end)
313
- if not sequence:
314
- raise ValueError("Could not get sequence for position")
315
-
316
- sequence_data = {
317
- 'sequence': sequence,
318
- 'start': start,
319
- 'end': end
320
- }
321
- self.logger.debug(f"Got position-based sequence of length: {len(sequence)}")
322
  except Exception as e:
323
- self.logger.error(f"Error getting position sequence: {str(e)}")
324
- QMessageBox.warning(self.view, "Error",
325
- "Could not get sequence for the specified position.")
 
 
 
326
  return
327
  else:
328
  # Regular gene-based search
@@ -336,9 +407,21 @@ class ViewTargetsController:
336
  QMessageBox.warning(self.view, "No Gene Data",
337
  "Could not get gene sequence for highlighting.")
338
  return
 
339
 
340
- self.logger.debug(f"Gene sequence length: {len(sequence_data['sequence'])}")
341
 
 
 
 
 
 
 
 
 
 
 
 
342
  # Highlight the sequences
343
  if guides_to_highlight:
344
  self.logger.debug("Attempting to highlight sequences")
@@ -349,7 +432,8 @@ class ViewTargetsController:
349
  "Could not get sequence information from the selected rows.")
350
 
351
  except Exception as e:
352
- self.logger.error(f"Error in highlight_gene_viewer: {str(e)}\n{traceback.format_exc()}")
 
353
  show_error(self.settings, "Error highlighting gene viewer", str(e))
354
 
355
  def export_targets(self):
@@ -440,12 +524,13 @@ class ViewTargetsController:
440
  return
441
 
442
  # Get sequence for new range
443
- if "chrom" in current_gene and "start:" in current_gene:
444
  # For position-based searches
445
  try:
446
  parts = current_gene.split(',')
447
- chrom = int(parts[0].split('chrom')[1].strip())
448
-
 
449
  # Get sequence for new range
450
  sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
451
 
@@ -476,6 +561,8 @@ class ViewTargetsController:
476
  "Could not get gene data for the current selection."
477
  )
478
  return
 
 
479
 
480
  # Get new sequence for the range
481
  sequence_data = self.model.get_gene_sequence_for_range(locus_tag, new_start, new_end)
@@ -504,11 +591,11 @@ class ViewTargetsController:
504
  current_gene = self.view.combo_box_gene.currentText()
505
 
506
  # Check if this is a position-based search
507
- if "chrom" in current_gene and "start:" in current_gene:
508
  try:
509
- # Parse position from the text (format: "chrom X, start: Y, end: Z")
510
  parts = current_gene.split(',')
511
- chrom = int(parts[0].split('chrom')[1].strip())
512
  start = int(parts[1].split('start:')[1].strip())
513
  end = int(parts[2].split('end:')[1].strip())
514
 
@@ -518,8 +605,8 @@ class ViewTargetsController:
518
  # Update gene viewer with sequence
519
  self.view.set_text_edit_gene_viewer(sequence)
520
 
521
- # Update location fields
522
- self.view.line_edit_start_location.setText(str(start))
523
  self.view.line_edit_stop_location.setText(str(end))
524
  else:
525
  raise ValueError("Could not get sequence for position")
@@ -541,8 +628,8 @@ class ViewTargetsController:
541
  # Update gene viewer with sequence
542
  self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
543
 
544
- # Update location fields
545
- self.view.line_edit_start_location.setText(str(sequence_data['start']))
546
  self.view.line_edit_stop_location.setText(str(sequence_data['end']))
547
  else:
548
  self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
@@ -596,171 +683,149 @@ class ViewTargetsController:
596
  def on_gene_selected(self, selected_text):
597
  """Handle gene selection signal"""
598
  try:
599
- self.logger.debug(f"Gene selection changed to: {selected_text}")
 
 
 
600
 
601
- # Check if this is a position-based search
602
- if "chrom" in selected_text and "start:" in selected_text:
603
- try:
604
- # Parse position from the text (format: "chrom X, start: Y, end: Z")
 
 
 
605
  parts = selected_text.split(',')
606
- chrom = int(parts[0].split('chrom')[1].strip())
607
  start = int(parts[1].split('start:')[1].strip())
608
  end = int(parts[2].split('end:')[1].strip())
609
 
610
- # Get sequence directly using _get_sequence_for_position
611
- sequence = self.model._get_sequence_for_position(chrom, start, end)
612
- if sequence:
613
- # Update gene viewer with sequence
614
- self.view.set_text_edit_gene_viewer(sequence)
615
-
616
- # Update location fields
617
- self.view.line_edit_start_location.setText(str(start))
618
- self.view.line_edit_stop_location.setText(str(end))
619
-
620
- self.logger.debug(f"Updated position view with sequence of length: {len(sequence)}")
621
-
622
- # Filter guides for this position
623
- position_guides = [g for g in self.model.guides
624
- if g.get('feature_id') == selected_text]
625
- self.view.display_guides_in_table(position_guides)
626
- else:
627
- self.logger.warning(f"No sequence found for position {chrom}:{start}-{end}")
628
- self.view.set_text_edit_gene_viewer("No sequence data available for this position")
629
- self.view.line_edit_start_location.clear()
630
- self.view.line_edit_stop_location.clear()
631
- except Exception as e:
632
- self.logger.error(f"Error handling position selection: {str(e)}")
633
- self.logger.error(f"Stack trace: {traceback.format_exc()}")
634
- else:
635
- # Regular gene-based search
636
- locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
637
- self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
638
-
639
- # Get gene sequence with padding using locus tag
640
- sequence_data = self.model.get_gene_sequence(locus_tag)
641
- if sequence_data:
642
- # Update gene viewer with sequence
643
- self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
644
-
645
  # Update location fields
646
- self.view.line_edit_start_location.setText(str(sequence_data['start']))
647
- self.view.line_edit_stop_location.setText(str(sequence_data['end']))
648
 
649
- self.logger.debug(f"Updated gene viewer with sequence of length: {len(sequence_data['sequence'])}")
 
 
 
 
 
650
 
651
- # Filter guides for this gene
652
- gene_guides = [g for g in self.model.guides
653
- if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
654
- self.view.display_guides_in_table(gene_guides)
655
  else:
656
- self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
657
- self.view.set_text_edit_gene_viewer("No sequence data available for this gene")
658
- self.view.line_edit_start_location.clear()
659
- self.view.line_edit_stop_location.clear()
660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  except Exception as e:
662
  self.logger.error(f"Error handling gene selection: {str(e)}")
663
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
664
 
665
- def highlight_guides_in_gene_viewer(self, guides_to_highlight=None):
666
  """Highlight selected guides in gene viewer"""
667
  try:
668
- self.logger.debug("Starting highlight_gene_viewer")
669
-
670
- if guides_to_highlight is None:
671
- guides_to_highlight = self.view.get_selected_guides()
672
-
673
- self.logger.debug(f"Selected guides: {guides_to_highlight}")
674
-
675
- if not guides_to_highlight:
676
- QMessageBox.warning(self.view, "No Selection",
677
- "Please select guides to highlight in the gene viewer.")
678
- return
679
-
680
- # Get current gene sequence
681
- selected_text = self.view.combo_box_gene.currentText()
682
 
683
- # For position-based searches, get sequence directly from model
684
- if "chrom" in selected_text and "start:" in selected_text:
685
- try:
686
- # Parse position from the text (format: "chrom X, start: Y, end: Z")
687
- parts = selected_text.split(',')
688
- chrom = int(parts[0].split('chrom')[1].strip())
689
- start = int(parts[1].split('start:')[1].strip())
690
- end = int(parts[2].split('end:')[1].strip())
691
-
692
- # Get sequence directly from FindTargetsModel
693
- sequence = self.model._get_sequence_for_position(chrom, start, end)
694
- if not sequence:
695
- raise ValueError("Could not get sequence for position")
696
-
697
- self.logger.debug(f"Got sequence of length {len(sequence)} for position-based search")
698
-
699
- except Exception as e:
700
- self.logger.error(f"Error parsing position or getting sequence: {str(e)}")
701
- return
702
  else:
703
- # Regular gene-based search
704
- locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
705
  sequence_data = self.model.get_gene_sequence(locus_tag)
706
- if not sequence_data or 'sequence' not in sequence_data:
707
- self.logger.error("No sequence data available for highlighting")
708
- return
709
- sequence = sequence_data['sequence']
710
-
711
- # Process highlights
712
- highlights = []
713
- sequences_found = 0
714
- total_sequences = len(guides_to_highlight)
715
-
716
- for guide in guides_to_highlight:
717
- self.logger.debug(f"Processing guide: {guide}")
718
- sequence_to_find = guide['sequence']
719
- strand = guide['strand']
720
-
721
- if strand == '-':
722
- sequence_to_find = str(Seq(sequence_to_find).reverse_complement())
723
- self.logger.debug(f"Reverse complemented sequence: {sequence_to_find}")
724
-
725
- sequence_upper = sequence.upper()
726
- target_upper = sequence_to_find.upper()
727
 
728
- self.logger.debug(f"Searching for sequence: {target_upper}")
729
-
730
- pos = sequence_upper.find(target_upper)
731
- if pos != -1:
732
- self.logger.debug(f"Found sequence at position: {pos}")
733
- color = 'red' if strand == '-' else 'green'
734
- highlights.append((pos, len(sequence_to_find), color))
735
- sequences_found += 1
736
- else:
737
- self.logger.debug(f"Sequence not found: {target_upper}")
738
-
739
- if sequences_found == 0:
740
- self.logger.warning("No sequences could be highlighted")
741
- QMessageBox.warning(self.view, "Highlighting Failed",
742
- "Could not highlight any of the selected sequences in the current gene view.")
743
  return
744
-
745
- # Build highlighted sequence
746
- result = []
747
- last_pos = 0
748
- for pos, length, color in sorted(highlights):
749
- result.append(sequence[last_pos:pos])
750
- result.append(f"<span style='background-color: {color};'>")
751
- result.append(sequence[pos:pos+length])
752
- result.append("</span>")
753
- last_pos = pos + length
754
 
755
- result.append(sequence[last_pos:])
756
- highlighted_sequence = ''.join(result)
757
 
758
- # Update the view with highlighted sequence
759
- self.view.update_gene_viewer(highlighted_sequence)
760
- self.logger.debug(f"Successfully highlighted {sequences_found} sequences")
761
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  except Exception as e:
763
- self.logger.error(f"Error highlighting guides: {str(e)}")
764
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
765
 
766
  def update_scores(self, scores, algorithm):
@@ -845,11 +910,11 @@ class ViewTargetsController:
845
  current_gene = self.view.combo_box_gene.currentText()
846
 
847
  # Reset gene viewer to original sequence
848
- if "chrom" in current_gene and "start:" in current_gene:
849
  # For position-based searches
850
  try:
851
  parts = current_gene.split(',')
852
- chrom = int(parts[0].split('chrom')[1].strip())
853
  start = int(parts[1].split('start:')[1].strip())
854
  end = int(parts[2].split('end:')[1].strip())
855
 
@@ -995,3 +1060,45 @@ class ViewTargetsController:
995
  self.logger.error(f"Error handling co-targeting result: {str(e)}")
996
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
997
  show_error(self.settings, "Co-targeting Error", str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from controllers.ScoringOptionsController import ScoringOptionsController
2
  from models.ViewTargetsModel import ViewTargetsModel
3
  from views.ViewTargetsView import ViewTargetsView
4
  from PyQt6.QtWidgets import QMessageBox
5
  from utils.ui import show_error
6
+ from PyQt6 import QtWidgets
 
7
  import traceback
8
+ from views.LoadingDialog import LoadingDialog
9
+ from PyQt6.QtWidgets import QApplication
10
+ from PyQt6.QtGui import QColor
11
 
12
  class ViewTargetsController:
13
  def __init__(self, global_settings):
 
36
 
37
  self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
38
  self.view.spin_box_minimum_on_target_score.valueChanged.connect(self.refresh_guides_display)
39
+ self.view.check_box_view_exons_only.stateChanged.connect(self._on_view_exons_changed)
40
 
41
+ def _on_view_exons_changed(self, state):
42
+ """Handle view exons only checkbox state change"""
43
+ try:
44
+ is_checked = self.view.check_box_view_exons_only.isChecked()
45
+ self.logger.debug(f"View exons only changed to: {is_checked}")
46
+ self.model.set_view_exons_only(is_checked)
47
+ self.refresh_gene_viewer()
48
+ except Exception as e:
49
+ self.logger.error(f"Error handling view exons change: {str(e)}")
50
+
51
+ def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
52
  try:
53
  self.organism = organism
54
  self.endonuclease = endonuclease
55
  self.selected_targets = selected_targets
56
 
57
+ # Use existing loading dialog if provided, otherwise create new one
58
+ using_existing_dialog = loading_dialog is not None
59
+ if not loading_dialog:
60
+ loading_dialog = LoadingDialog(self.view, "Loading guides...")
61
+ loading_dialog.show()
62
+ QApplication.processEvents()
63
 
64
+ try:
65
+ loading_dialog.set_message("Loading guides...", 60)
66
+ QApplication.processEvents()
67
+
68
+ self.model.load_guides(selected_targets, organism, endonuclease)
69
+
70
+ # Initialize endonuclease combo box
71
+ loading_dialog.set_message("Setting up endonucleases...", 65)
72
+ QApplication.processEvents()
73
 
74
+ org_to_endo = self.settings.get_organism_to_endonuclease()
75
+ if organism in org_to_endo:
76
+ available_endos = org_to_endo[organism]
77
+ self.view.combo_box_endonuclease.clear()
78
+ self.view.combo_box_endonuclease.addItems(available_endos)
79
 
80
+ # Set current endonuclease
81
+ current_index = self.view.combo_box_endonuclease.findText(endonuclease)
82
+ if current_index >= 0:
83
+ self.view.combo_box_endonuclease.setCurrentIndex(current_index)
84
+
85
+ self.view.combo_box_endonuclease.currentTextChanged.connect(self._on_endonuclease_changed)
86
+
87
+ loading_dialog.set_message("Processing guides...", 70)
88
+ QApplication.processEvents()
89
+
90
+ guides = self.model.get_guides()
 
 
91
 
92
+ loading_dialog.set_message("Updating display...", 80)
93
+ QApplication.processEvents()
94
+
95
+ self.view.display_guides_in_table(guides)
96
+
97
+ # Only trigger gene selection if this is the initial load
98
+ if not hasattr(self, '_initial_load_complete'):
99
+ loading_dialog.set_message("Loading initial gene data...", 90)
100
+ QApplication.processEvents()
101
 
102
+ # Get unique position names or gene IDs
103
+ unique_entries = set()
104
+ for target in selected_targets:
105
+ if 'feature_id' in target:
106
+ # For position-based searches, use the feature_id directly
107
+ if "chromosome" in str(target['feature_id']):
108
+ unique_entries.add(target['feature_id'])
109
+ else:
110
+ # For gene-based searches, get gene data and format with name
111
+ locus_tag = target['feature_id']
112
+ gene_data = self.model.get_gene_data(locus_tag)
113
+ if gene_data and 'info' in gene_data:
114
+ gene_name = gene_data['info'].get('gene_name', '')
115
+ display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
116
+ unique_entries.add(display_text)
117
 
118
+ # Convert set to list for combo box
119
+ entries = list(unique_entries)
120
+ self.logger.debug(f"Found {len(entries)} unique entries")
121
+
122
+ self.view.set_combo_box_gene(entries)
123
+
124
+ # Set first entry without triggering the selection signal
125
+ if entries:
126
+ self.view.combo_box_gene.blockSignals(True)
127
+ self.view.combo_box_gene.setCurrentIndex(0)
128
+ self.view.combo_box_gene.blockSignals(False)
129
+ self._load_initial_gene_data(entries[0])
130
+
131
+ self._initial_load_complete = True
132
+
133
+ loading_dialog.set_progress(100)
134
+ QApplication.processEvents()
135
+
136
+ finally:
137
+ # Only close the dialog if we created it
138
+ if not using_existing_dialog:
139
+ loading_dialog.close()
140
+ QApplication.processEvents()
141
+
142
  except Exception as e:
143
  self.logger.error(f"Error in load_guides: {str(e)}")
144
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
145
  show_error(self.settings, "Error loading guides", str(e))
146
 
147
+ def _load_initial_gene_data(self, selected_text):
148
+ """Load initial gene data without showing loading dialog"""
149
+ try:
150
+ # Similar to on_gene_selected but without loading dialog
151
+ if "chromosome" in selected_text and "start:" in selected_text:
152
+ # Parse position from the text
153
+ parts = selected_text.split(',')
154
+ chrom = parts[0].split('chromosome')[1].strip() # Remove any extra colons
155
+ start = int(parts[1].split('start:')[1].strip())
156
+ end = int(parts[2].split('end:')[1].strip())
157
+
158
+ self.view.line_edit_start_location.setText(str(start))
159
+ self.view.line_edit_stop_location.setText(str(end))
160
+
161
+ # Get sequence directly for position-based search
162
+ sequence = self.model._get_sequence_for_position(chrom, start, end)
163
+ if sequence:
164
+ # Update gene viewer with sequence
165
+ self.view.update_gene_viewer(sequence, [])
166
+ self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
167
+ else:
168
+ self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
169
+
170
+ position_guides = [g for g in self.model.guides
171
+ if g.get('feature_id') == selected_text]
172
+ self.view.display_guides_in_table(position_guides)
173
+ else:
174
+ # Regular gene-based search
175
+ locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
176
+ sequence_data = self.model.get_gene_sequence(locus_tag)
177
+ if sequence_data:
178
+ self.view.line_edit_start_location.setText(str(sequence_data['start']))
179
+ self.view.line_edit_stop_location.setText(str(sequence_data['end']))
180
+
181
+ features = self.model.get_features_for_gene(locus_tag)
182
+
183
+ self.view.update_gene_viewer(sequence_data['sequence'], features)
184
+
185
+ gene_guides = [g for g in self.model.guides
186
+ if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
187
+ self.view.display_guides_in_table(gene_guides)
188
+
189
+ self.logger.debug("Initial gene data loaded successfully")
190
+
191
+ except Exception as e:
192
+ self.logger.error(f"Error loading initial gene data: {str(e)}")
193
+
194
  def _on_endonuclease_changed(self, new_endonuclease):
195
  try:
196
  if new_endonuclease != self.endonuclease:
 
226
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
227
  show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
228
 
229
+ # def load_gene_viewer(self):
230
+ # try:
231
+ # # Get selected gene from combo box
232
+ # selected_text = self.view.combo_box_gene.currentText()
233
+ # if not selected_text:
234
+ # self.logger.debug("No gene selected")
235
+ # return
236
 
237
+ # # Extract locus tag from "locus_tag: gene_name" format
238
+ # locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
239
+ # self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
 
 
240
 
241
+ # # Get gene sequence with padding
242
+ # sequence_data = self.model.get_gene_sequence(locus_tag)
 
243
 
244
+ # if sequence_data:
245
+ # # Update gene viewer with sequence
246
+ # self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
 
 
 
247
 
248
+ # # Update location fields
249
+ # self.view.line_edit_start_location.setText(str(sequence_data['start']))
250
+ # self.view.line_edit_stop_location.setText(str(sequence_data['end']))
251
 
252
+ # else:
253
+ # self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
254
 
255
+ # except Exception as e:
256
+ # self.logger.error(f"Error in load_gene_viewer: {str(e)}")
257
+ # self.logger.error(f"Stack trace: {traceback.format_exc()}")
258
 
259
  def perform_off_target_analysis(self):
260
  """Launch off-target analysis for selected guides"""
 
299
  def _handle_off_target_results(self, results):
300
  """Handle off-target analysis results"""
301
  try:
302
+ scores, details = results
303
 
 
304
  headers = self.view.get_table_headers()
305
 
306
  # Find Score column index
 
367
  "Please select guides to highlight in the gene viewer.")
368
  return
369
 
370
+ # Get current gene/position
 
 
 
 
 
 
 
 
 
 
 
371
  current_gene = self.view.combo_box_gene.currentText()
372
+ sequence = None
373
 
374
  # Check if this is a position-based search
375
+ if "chromosome" in current_gene and "start:" in current_gene:
376
+ # Parse position from the text
377
  try:
378
  parts = current_gene.split(',')
379
+ chrom = parts[0].split('chromosome')[1].strip()
380
  start = int(parts[1].split('start:')[1].strip())
381
  end = int(parts[2].split('end:')[1].strip())
382
 
383
+ # Get sequence directly for position-based search
384
  sequence = self.model._get_sequence_for_position(chrom, start, end)
385
+ if sequence:
386
+ self.logger.debug(f"Got sequence of length {len(sequence)} for position-based search")
387
+ else:
388
+ raise ValueError(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
389
+
 
 
 
 
390
  except Exception as e:
391
+ self.logger.error(f"Error parsing position or getting sequence: {str(e)}")
392
+ QMessageBox.warning(
393
+ self.view,
394
+ "Sequence Error",
395
+ f"Could not get sequence for the selected position: {str(e)}"
396
+ )
397
  return
398
  else:
399
  # Regular gene-based search
 
407
  QMessageBox.warning(self.view, "No Gene Data",
408
  "Could not get gene sequence for highlighting.")
409
  return
410
+ sequence = sequence_data['sequence']
411
 
412
+ self.logger.debug(f"Got sequence of length: {len(sequence)}")
413
 
414
+ # Convert table selections to the format expected by the model
415
+ guides_to_highlight = []
416
+ for guide in selected_rows:
417
+ guide_info = {
418
+ 'location': guide['location'],
419
+ 'sequence': guide['sequence'],
420
+ 'strand': guide['strand']
421
+ }
422
+ guides_to_highlight.append(guide_info)
423
+ self.logger.debug(f"Guide to highlight: {guide_info}")
424
+
425
  # Highlight the sequences
426
  if guides_to_highlight:
427
  self.logger.debug("Attempting to highlight sequences")
 
432
  "Could not get sequence information from the selected rows.")
433
 
434
  except Exception as e:
435
+ self.logger.error(f"Error in highlight_gene_viewer: {str(e)}")
436
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
437
  show_error(self.settings, "Error highlighting gene viewer", str(e))
438
 
439
  def export_targets(self):
 
524
  return
525
 
526
  # Get sequence for new range
527
+ if "chromosome" in current_gene and "start:" in current_gene:
528
  # For position-based searches
529
  try:
530
  parts = current_gene.split(',')
531
+ # Get full chromosome identifier instead of just the number
532
+ chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
533
+
534
  # Get sequence for new range
535
  sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
536
 
 
561
  "Could not get gene data for the current selection."
562
  )
563
  return
564
+
565
+ print(f"locus_tag: {locus_tag}, new_start: {new_start}, new_end: {new_end}")
566
 
567
  # Get new sequence for the range
568
  sequence_data = self.model.get_gene_sequence_for_range(locus_tag, new_start, new_end)
 
591
  current_gene = self.view.combo_box_gene.currentText()
592
 
593
  # Check if this is a position-based search
594
+ if "chromosome" in current_gene and "start:" in current_gene:
595
  try:
596
+ # Parse position from the text
597
  parts = current_gene.split(',')
598
+ chrom = parts[0].split('chromosome')[1].strip() # Keep full chromosome ID
599
  start = int(parts[1].split('start:')[1].strip())
600
  end = int(parts[2].split('end:')[1].strip())
601
 
 
605
  # Update gene viewer with sequence
606
  self.view.set_text_edit_gene_viewer(sequence)
607
 
608
+ # Update location fields - subtract 1 from start to match 0-based indexing
609
+ self.view.line_edit_start_location.setText(str(start + 1))
610
  self.view.line_edit_stop_location.setText(str(end))
611
  else:
612
  raise ValueError("Could not get sequence for position")
 
628
  # Update gene viewer with sequence
629
  self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
630
 
631
+ # Update location fields - subtract 1 from start to match 0-based indexing
632
+ self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
633
  self.view.line_edit_stop_location.setText(str(sequence_data['end']))
634
  else:
635
  self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
 
683
  def on_gene_selected(self, selected_text):
684
  """Handle gene selection signal"""
685
  try:
686
+ # Create loading dialog
687
+ loading_dialog = LoadingDialog(self.view, "Loading gene data...")
688
+ loading_dialog.show()
689
+ QApplication.processEvents()
690
 
691
+ try:
692
+ # Load data in chunks
693
+ loading_dialog.set_message("Loading sequence data...", 30)
694
+ QApplication.processEvents()
695
+
696
+ if "chromosome" in selected_text and "start:" in selected_text:
697
+ # Handle position-based search
698
  parts = selected_text.split(',')
699
+ chrom = parts[0].split('chromosome')[1].strip()
700
  start = int(parts[1].split('start:')[1].strip())
701
  end = int(parts[2].split('end:')[1].strip())
702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  # Update location fields
704
+ self.view.line_edit_start_location.setText(str(start))
705
+ self.view.line_edit_stop_location.setText(str(end))
706
 
707
+ # Filter guides efficiently
708
+ loading_dialog.set_message("Filtering guides...", 60)
709
+ QApplication.processEvents()
710
+ position_guides = [g for g in self.model.guides
711
+ if g.get('feature_id') == selected_text]
712
+ self.view.display_guides_in_table(position_guides)
713
 
 
 
 
 
714
  else:
715
+ # Regular gene-based search with optimized loading
716
+ locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
717
+ sequence_data = self.model.get_gene_sequence(locus_tag)
 
718
 
719
+ if sequence_data:
720
+ self.view.line_edit_start_location.setText(str(sequence_data['start']))
721
+ self.view.line_edit_stop_location.setText(str(sequence_data['end']))
722
+
723
+ loading_dialog.set_message("Updating display...", 80)
724
+ QApplication.processEvents()
725
+
726
+ gene_guides = [g for g in self.model.guides
727
+ if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
728
+ self.view.display_guides_in_table(gene_guides)
729
+
730
+ # Refresh gene viewer
731
+ loading_dialog.set_message("Refreshing viewer...", 90)
732
+ QApplication.processEvents()
733
+ self.refresh_gene_viewer()
734
+
735
+ finally:
736
+ loading_dialog.close()
737
+
738
  except Exception as e:
739
  self.logger.error(f"Error handling gene selection: {str(e)}")
740
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
741
 
742
+ def highlight_guides_in_gene_viewer(self, guides_to_highlight):
743
  """Highlight selected guides in gene viewer"""
744
  try:
745
+ # Get current sequence
746
+ current_gene = self.view.combo_box_gene.currentText()
747
+ sequence_data = None
 
 
 
 
 
 
 
 
 
 
 
748
 
749
+ # Get sequence based on view type
750
+ if "chromosome" in current_gene and "start:" in current_gene:
751
+ parts = current_gene.split(',')
752
+ chrom = parts[0].split('chromosome')[1].strip()
753
+ start = int(parts[1].split('start:')[1].strip())
754
+ end = int(parts[2].split('end:')[1].strip())
755
+ sequence = self.model._get_sequence_for_position(chrom, start, end)
756
+ if sequence:
757
+ sequence_data = {'sequence': sequence}
 
 
 
 
 
 
 
 
 
 
758
  else:
759
+ locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
 
760
  sequence_data = self.model.get_gene_sequence(locus_tag)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
+ if not sequence_data or 'sequence' not in sequence_data:
763
+ self.logger.error("No sequence data available")
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  return
 
 
 
 
 
 
 
 
 
 
765
 
766
+ sequence = sequence_data['sequence']
767
+ sequence_upper = sequence.upper()
768
 
769
+ # Clear existing highlights
770
+ self.view.dna_feature_viewer.sequence_viewer.clear_highlights()
 
771
 
772
+ for guide in guides_to_highlight:
773
+ try:
774
+ print(f"Guide: {guide}")
775
+ guide_sequence = guide['sequence']
776
+ strand = guide['strand']
777
+ print(f"Strand: {strand}")
778
+
779
+ # For negative strand guides
780
+ if strand == '-':
781
+ print("Negative strand")
782
+ # Convert sequence to complement for negative strand search
783
+ print(f"Sequence: {sequence_upper}")
784
+ complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'K': 'M', 'Y': 'R', 'R': 'Y', 'M': 'K', 'S': 'S'}[base] for base in sequence_upper)
785
+ print(f"Complement sequence: {complement_sequence}")
786
+ target_sequence = guide_sequence.upper()
787
+ print(f"Target sequence: {target_sequence}")
788
+ target_sequence = target_sequence[::-1]
789
+ print(f"Reversed target sequence: {target_sequence}")
790
+ pos = complement_sequence.find(target_sequence)
791
+ print(f"Position: {pos}")
792
+ if pos != -1:
793
+ color = QColor(255, 0, 0, 100) # Red for negative strand
794
+ self.logger.debug(f"Found negative strand sequence at position {pos}")
795
+
796
+ # Pass the original position but indicate negative strand
797
+ self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
798
+ pos,
799
+ pos + len(guide_sequence) - 1,
800
+ color,
801
+ strand='-'
802
+ )
803
+ else:
804
+ self.logger.warning(f"Negative strand sequence {target_sequence} not found")
805
+ else:
806
+ # For positive strand guides
807
+ target_sequence = guide_sequence.upper()
808
+ pos = sequence_upper.find(target_sequence)
809
+
810
+ if pos != -1:
811
+ color = QColor(0, 255, 0, 100) # Green for positive strand
812
+ self.logger.debug(f"Found positive strand sequence at position {pos}")
813
+
814
+ self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
815
+ pos,
816
+ pos + len(guide_sequence) - 1,
817
+ color,
818
+ strand='+'
819
+ )
820
+ else:
821
+ self.logger.warning(f"Positive strand sequence {target_sequence} not found")
822
+
823
+ except Exception as e:
824
+ self.logger.error(f"Error highlighting guide: {str(e)}")
825
+ continue
826
+
827
  except Exception as e:
828
+ self.logger.error(f"Error in highlight_guides_in_gene_viewer: {str(e)}")
829
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
830
 
831
  def update_scores(self, scores, algorithm):
 
910
  current_gene = self.view.combo_box_gene.currentText()
911
 
912
  # Reset gene viewer to original sequence
913
+ if "chromosome" in current_gene and "start:" in current_gene:
914
  # For position-based searches
915
  try:
916
  parts = current_gene.split(',')
917
+ chrom = parts[0].split('chromosome')[1].strip() # Keep full chromosome ID
918
  start = int(parts[1].split('start:')[1].strip())
919
  end = int(parts[2].split('end:')[1].strip())
920
 
 
1060
  self.logger.error(f"Error handling co-targeting result: {str(e)}")
1061
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
1062
  show_error(self.settings, "Co-targeting Error", str(e))
1063
+
1064
+ def refresh_gene_viewer(self):
1065
+ """Refresh gene viewer with sequence and features"""
1066
+ try:
1067
+ current_gene = self.view.combo_box_gene.currentText()
1068
+ if not current_gene:
1069
+ return
1070
+
1071
+ self.logger.debug("Refreshing gene viewer")
1072
+ is_exons_only = self.view.check_box_view_exons_only.isChecked()
1073
+ self.logger.debug(f"View exons only is: {is_exons_only}")
1074
+
1075
+ # Get gene data
1076
+ if "chromosome" in current_gene and "start:" in current_gene:
1077
+ # Handle position-based search
1078
+ parts = current_gene.split(',')
1079
+ chrom = parts[0].split('chromosome')[1].strip()
1080
+ start = int(parts[1].split('start:')[1].strip())
1081
+ end = int(parts[2].split('end:')[1].strip())
1082
+ sequence = self.model._get_sequence_for_position(chrom, start, end)
1083
+
1084
+ if sequence:
1085
+ # Get features for this region
1086
+ features = self.model.get_features_for_region(chrom, start, end)
1087
+ self.view.update_gene_viewer(sequence, features)
1088
+ else:
1089
+ # Regular gene-based search
1090
+ locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
1091
+ self.logger.debug(f"Getting sequence for locus tag: {locus_tag}")
1092
+ sequence_data = self.model.get_gene_sequence(locus_tag)
1093
+
1094
+ if sequence_data and 'sequence' in sequence_data:
1095
+ # Get features for this gene
1096
+ features = self.model.get_features_for_gene(locus_tag)
1097
+ self.view.update_gene_viewer(sequence_data['sequence'], features)
1098
+ self.logger.debug(f"Updated gene viewer with sequence and {len(features)} features")
1099
+ else:
1100
+ self.logger.warning("No sequence data available")
1101
+
1102
+ except Exception as e:
1103
+ self.logger.error(f"Error refreshing gene viewer: {str(e)}")
1104
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
src/models/AnnotationParser.py CHANGED
@@ -1,11 +1,9 @@
 
1
  from PyQt6.QtWidgets import QMessageBox
2
  from Bio import SeqIO
3
  import os
4
  import traceback
5
- from functools import lru_cache
6
- import json
7
  import pickle
8
- import time
9
 
10
  class AnnotationParser:
11
  def __init__(self, global_settings):
@@ -21,15 +19,22 @@ class AnnotationParser:
21
  def set_annotation_file(self, file_path):
22
  """Set the annotation file and initialize/load index"""
23
  try:
24
- # Don't process if file_path is a directory or empty
25
- if not file_path or os.path.isdir(file_path):
26
- self.logger.debug(f"Invalid annotation file path: {file_path}")
27
  self._index = {'locus_tags': {}} # Initialize empty index
28
  return
29
 
 
 
 
 
 
 
 
 
 
30
  if self.annotation_file_name != file_path:
31
- total_start = time.time()
32
-
33
  self.annotation_file_name = file_path
34
  self.logger.debug(f"Set annotation file to: {file_path}")
35
 
@@ -37,15 +42,9 @@ class AnnotationParser:
37
  self.index_file = f"{file_path}.index"
38
 
39
  # Load or create index
40
- index_start = time.time()
41
  if not self._load_index():
42
  self.logger.debug("Index not found or outdated, creating new index...")
43
- create_start = time.time()
44
  self._create_index()
45
- create_time = time.time() - create_start
46
- self.logger.debug(f"Index creation time: {create_time:.2f} seconds")
47
- index_time = time.time() - index_start
48
- self.logger.debug(f"Total index handling time: {index_time:.2f} seconds")
49
 
50
  except Exception as e:
51
  self.logger.error(f"Error in set_annotation_file: {str(e)}")
@@ -53,7 +52,6 @@ class AnnotationParser:
53
 
54
  def _create_index(self):
55
  try:
56
- start_time = time.time()
57
  self.logger.debug("Creating gene index file...")
58
 
59
  # Initialize optimized index structure - no sequences stored
@@ -65,13 +63,22 @@ class AnnotationParser:
65
  record_count = 0
66
  feature_count = 0
67
 
 
 
 
 
 
 
 
 
 
 
68
  for record in SeqIO.parse(self.annotation_file_name, "genbank"):
69
  record_count += 1
70
- record_start = time.time()
71
 
72
  # Process features
73
  for feature in record.features:
74
- if feature.type in ['CDS', 'gene']:
75
  feature_count += 1
76
 
77
  # Get essential feature info
@@ -80,44 +87,104 @@ class AnnotationParser:
80
  locus_tag = feature.qualifiers['locus_tag'][0]
81
  elif 'gene' in feature.qualifiers:
82
  locus_tag = feature.qualifiers['gene'][0]
83
-
84
  # Only process features with valid locus tags
85
  if locus_tag and locus_tag.lower() != "n/a":
86
- # Get location info
87
- start = int(feature.location.start)
88
- end = int(feature.location.end)
89
- strand = '+' if feature.location.strand == 1 else '-'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- # Store feature info with full names
92
  feature_entry = {
93
  'feature_type': feature.type,
94
  'chromosome': record.id,
95
  'location': f"{start}:{end}({strand})",
96
- 'gene_name': feature.qualifiers.get('gene', ['N/A'])[0],
97
- 'description': feature.qualifiers.get('product',
98
- feature.qualifiers.get('note', ['N/A']))[0],
99
  'start': start,
100
  'end': end
101
  }
102
-
103
- # Store in index
104
- index_data['locus_tags'][locus_tag] = feature_entry
105
-
106
- record_time = time.time() - record_start
107
- if record_count % 100 == 0:
108
- self.logger.debug(f"Processed {record_count} records, {feature_count} features. Last record time: {record_time:.2f}s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  # Save compressed index to file
111
- save_start = time.time()
112
  with open(self.index_file, 'wb') as f:
113
  pickle.dump(index_data, f, protocol=pickle.HIGHEST_PROTOCOL)
114
- save_time = time.time() - save_start
115
- total_time = time.time() - start_time
116
-
117
  self._index = index_data
118
-
 
 
 
 
119
  self.logger.debug(f"Index creation complete. Records: {record_count}, Features: {feature_count}")
120
- self.logger.debug(f"Save time: {save_time:.2f}s, Total time: {total_time:.2f}s")
121
  return True
122
 
123
  except Exception as e:
@@ -125,21 +192,17 @@ class AnnotationParser:
125
  return False
126
 
127
  def _load_index(self):
128
- """Load the index file if it exists and is newer than the GenBank file"""
129
  try:
 
130
  if not os.path.exists(self.index_file):
131
  return False
132
-
133
  # Check if index is older than GenBank file
134
  if os.path.getmtime(self.index_file) < os.path.getmtime(self.annotation_file_name):
135
  return False
136
 
137
- start_time = time.time()
138
  with open(self.index_file, 'rb') as f:
139
  self._index = pickle.load(f)
140
- load_time = time.time() - start_time
141
- print(f"Index file: {self._index}")
142
- self.logger.debug(f"Index file loaded successfully in {load_time:.2f} seconds")
143
  return True
144
 
145
  except Exception as e:
@@ -162,16 +225,7 @@ class AnnotationParser:
162
 
163
  # Search through index
164
  if hasattr(self, '_index') and 'locus_tags' in self._index:
165
- # Search through features, filtering for CDS and gene types only
166
  for locus_tag, feature_entry in self._index['locus_tags'].items():
167
- # Safely get feature type with default value
168
- feature_type = feature_entry.get('feature_type', '')
169
-
170
- # Only process CDS and gene features
171
- if feature_type not in ['CDS', 'gene']:
172
- continue
173
-
174
- # Check gene name, locus tag, and description
175
  searchable_text = ' '.join([
176
  feature_entry.get('gene_name', '').lower(),
177
  locus_tag.lower(),
@@ -183,8 +237,10 @@ class AnnotationParser:
183
  info = {
184
  'feature_id': locus_tag,
185
  'feature_name': feature_entry.get('gene_name', 'N/A'),
 
186
  'feature_location': feature_entry.get('location', 'N/A'),
187
- 'feature_description': feature_entry.get('description', 'N/A')
 
188
  }
189
  results_list.append((feature_entry.get('chromosome', ''), info))
190
 
@@ -192,7 +248,7 @@ class AnnotationParser:
192
 
193
  except Exception as e:
194
  self.logger.error(f"Error in genbank_search: {str(e)}")
195
- self.logger.error(f"Stack trace: {traceback.format_exc()}") # Add stack trace for better debugging
196
  raise
197
 
198
  def get_gene_data(self, gene_identifier):
@@ -219,6 +275,7 @@ class AnnotationParser:
219
  'feature_type': gene_info['feature_type'],
220
  'chromosome': gene_info['chromosome'],
221
  'location': gene_info['location'],
 
222
  'gene_name': gene_info['gene_name'],
223
  'description': gene_info['description'],
224
  'start': gene_info['start'],
@@ -241,6 +298,7 @@ class AnnotationParser:
241
  'feature_type': value['feature_type'],
242
  'chromosome': value['chromosome'],
243
  'location': value['location'],
 
244
  'gene_name': value['gene_name'],
245
  'description': value['description'],
246
  'start': value['start'],
@@ -274,128 +332,9 @@ class AnnotationParser:
274
  padded_sequence = sequence[start:end]
275
 
276
  self.logger.debug(f"Padded sequence: {padded_sequence}")
277
-
278
  return padded_sequence
279
-
280
  return None
281
 
282
  except Exception as e:
283
  self.logger.error(f"Error getting sequence for gene: {str(e)}")
284
- return None
285
-
286
- @lru_cache(maxsize=1)
287
- def _get_records(self):
288
- """Cache and return all records from the annotation file"""
289
- start_time = time.time()
290
- if not self._record_cache:
291
- try:
292
- self.logger.debug("Loading records from file...")
293
- self._record_cache = list(SeqIO.parse(self.annotation_file_name, "genbank"))
294
- load_time = time.time() - start_time
295
- self.logger.debug(f"Time to load records: {load_time:.2f} seconds")
296
- except Exception as e:
297
- self.logger.error(f"Error reading annotation file: {str(e)}")
298
- return []
299
- return self._record_cache
300
-
301
- def get_max_chrom(self):
302
- try:
303
- parser = SeqIO.parse(self.annotation_file_name, 'genbank')
304
- max_chrom = sum(1 for _ in parser)
305
- return max_chrom
306
- except Exception as e:
307
- self.logger.error(f"Error in get_max_chrom: {str(e)}")
308
- self._show_error("Error in get_max_chrom", str(e))
309
- return 0
310
-
311
- def get_sequence_info(self, query):
312
- # Implement this method if needed
313
- pass
314
-
315
- def find_which_file_version(self):
316
- try:
317
- if not self.annotation_file_name or os.path.basename(self.annotation_file_name) == "None":
318
- return -1
319
- if self.annotation_file_name.endswith(('.gbff', '.gbk')):
320
- return "gbff"
321
- else:
322
- return -1
323
- except Exception as e:
324
- self.logger.error(f"Error in find_which_file_version: {str(e)}")
325
- self._show_error("Error in find_which_file_version", str(e))
326
- return -1
327
-
328
- def _show_error(self, title, message):
329
- QMessageBox.critical(None, title, f"{message}\n\nFor more information, check the log file.")
330
-
331
- @staticmethod
332
- def flatten_list(t):
333
- return [item.lower() for sublist in t for item in sublist]
334
-
335
- def _get_feature_info(self, feature):
336
- return {
337
- 'feature_id': self._get_feature_id(feature),
338
- 'feature_name': self._get_feature_name(feature),
339
- 'feature_location': self._get_feature_location(feature),
340
- 'feature_description': self._get_feature_description(feature)
341
- }
342
-
343
- def _get_feature_id(self, feature):
344
- for key in ['locus_tag']:
345
- if key in feature.qualifiers:
346
- return feature.qualifiers[key][0]
347
- return "N/A"
348
-
349
- def _get_feature_name(self, feature):
350
- for key in ['gene']:
351
- if key in feature.qualifiers:
352
- return feature.qualifiers[key][0]
353
- return "N/A"
354
-
355
- def _get_feature_location(self, feature):
356
- if feature.location:
357
- start = feature.location.start
358
- end = feature.location.end
359
- strand = '+' if feature.location.strand == 1 else '-'
360
- return f"{start}:{end}({strand})"
361
- return "N/A"
362
-
363
- def _get_feature_description(self, feature):
364
- for key in ['product', 'note']:
365
- if key in feature.qualifiers:
366
- return feature.qualifiers[key][0]
367
- return "N/A"
368
-
369
- def get_available_genes(self):
370
- return self.available_genes
371
-
372
- def get_full_gene_sequence(self):
373
- # Implement this method if needed
374
- pass
375
-
376
- def _build_gene_index(self, records):
377
- """Build an index of genes for faster lookup"""
378
- self._gene_index = {}
379
- try:
380
- for record in records:
381
- for feature in record.features:
382
- if feature.type == 'gene':
383
- gene_name = self._get_feature_name(feature)
384
- gene_id = self._get_feature_id(feature)
385
- if gene_name != "N/A":
386
- self._gene_index[gene_name] = (record.id, feature)
387
- if gene_id != "N/A":
388
- self._gene_index[gene_id] = (record.id, feature)
389
- except Exception as e:
390
- self.logger.error(f"Error building gene index: {str(e)}")
391
-
392
- def _parse_available_genes(self):
393
- self.available_genes = []
394
- try:
395
- for record in SeqIO.parse(self.annotation_file_name, "genbank"):
396
- for feature in record.features:
397
- if feature.type == 'gene':
398
- self.available_genes.append(self._get_feature_name(feature))
399
- except Exception as e:
400
- self.logger.error(f"Error parsing available genes: {str(e)}")
401
-
 
1
+ import Bio
2
  from PyQt6.QtWidgets import QMessageBox
3
  from Bio import SeqIO
4
  import os
5
  import traceback
 
 
6
  import pickle
 
7
 
8
  class AnnotationParser:
9
  def __init__(self, global_settings):
 
19
  def set_annotation_file(self, file_path):
20
  """Set the annotation file and initialize/load index"""
21
  try:
22
+ # Don't process if file_path is empty
23
+ if not file_path:
24
+ self.logger.warning("Empty annotation file path provided")
25
  self._index = {'locus_tags': {}} # Initialize empty index
26
  return
27
 
28
+ # Normalize path and remove any trailing slashes
29
+ file_path = os.path.normpath(file_path)
30
+
31
+ # Verify file exists and is a file (not a directory)
32
+ if not os.path.isfile(file_path):
33
+ self.logger.error(f"Invalid annotation file path: {file_path}")
34
+ self._index = {'locus_tags': {}}
35
+ return
36
+
37
  if self.annotation_file_name != file_path:
 
 
38
  self.annotation_file_name = file_path
39
  self.logger.debug(f"Set annotation file to: {file_path}")
40
 
 
42
  self.index_file = f"{file_path}.index"
43
 
44
  # Load or create index
 
45
  if not self._load_index():
46
  self.logger.debug("Index not found or outdated, creating new index...")
 
47
  self._create_index()
 
 
 
 
48
 
49
  except Exception as e:
50
  self.logger.error(f"Error in set_annotation_file: {str(e)}")
 
52
 
53
  def _create_index(self):
54
  try:
 
55
  self.logger.debug("Creating gene index file...")
56
 
57
  # Initialize optimized index structure - no sequences stored
 
63
  record_count = 0
64
  feature_count = 0
65
 
66
+ # Priority order for feature types (higher index = higher priority)
67
+ feature_priority = {
68
+ 'CDS': 0,
69
+ 'gene': 1,
70
+ 'mRNA': 2,
71
+ 'tRNA': 2,
72
+ 'rRNA': 2,
73
+ 'ncRNA': 2
74
+ }
75
+
76
  for record in SeqIO.parse(self.annotation_file_name, "genbank"):
77
  record_count += 1
 
78
 
79
  # Process features
80
  for feature in record.features:
81
+ if feature.type in feature_priority:
82
  feature_count += 1
83
 
84
  # Get essential feature info
 
87
  locus_tag = feature.qualifiers['locus_tag'][0]
88
  elif 'gene' in feature.qualifiers:
89
  locus_tag = feature.qualifiers['gene'][0]
90
+
91
  # Only process features with valid locus tags
92
  if locus_tag and locus_tag.lower() != "n/a":
93
+ # Handle joined locations
94
+ if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
95
+ if locus_tag == "CAALFM_C304810CA":
96
+ print(f"Feature location: {feature.location}")
97
+ # Get all parts of the joined location
98
+ parts = feature.location.parts
99
+ # Find min start and max end across all parts
100
+ start = min(int(part.start) for part in parts)
101
+ end = max(int(part.end) for part in parts)
102
+
103
+ # Format parts with strand info
104
+ formatted_parts = [
105
+ f"{int(part.start)}..{int(part.end)}({'+' if part.strand == 1 else '-'})"
106
+ for part in parts
107
+ ]
108
+
109
+ # If on minus strand, reverse the order of parts
110
+ if feature.location.strand == -1:
111
+ formatted_parts.reverse()
112
+
113
+ # Join parts into full location string
114
+ full_location = ','.join(formatted_parts)
115
+
116
+ # Get overall strand for location field
117
+ strand = '+' if feature.location.strand == 1 else '-'
118
+ else:
119
+ start = int(feature.location.start)
120
+ end = int(feature.location.end)
121
+ strand = '+' if feature.location.strand == 1 else '-'
122
+ full_location = f"{start}..{end}({strand})"
123
+
124
+ # Get description first since we might need it for the name
125
+ description = feature.qualifiers.get('product',
126
+ feature.qualifiers.get('note', ['N/A']))[0]
127
+
128
+ # Get gene name, use description if gene name is N/A
129
+ gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
130
+ if gene_name == 'N/A':
131
+ gene_name = description # Use description as name if no gene name
132
 
133
+ # Create new feature entry
134
  feature_entry = {
135
  'feature_type': feature.type,
136
  'chromosome': record.id,
137
  'location': f"{start}:{end}({strand})",
138
+ 'full_location': full_location,
139
+ 'gene_name': gene_name,
140
+ 'description': description,
141
  'start': start,
142
  'end': end
143
  }
144
+
145
+ # Update index based on priority
146
+ if locus_tag in index_data['locus_tags']:
147
+ existing_entry = index_data['locus_tags'][locus_tag]
148
+ existing_priority = feature_priority[existing_entry['feature_type']]
149
+ current_priority = feature_priority[feature.type]
150
+
151
+ if current_priority >= existing_priority:
152
+ # Keep the RNA/higher priority feature type
153
+ merged_entry = existing_entry.copy()
154
+ merged_entry['feature_type'] = feature.type
155
+
156
+ # Update other fields only if they're not 'N/A'
157
+ if feature_entry['gene_name'] != 'N/A':
158
+ merged_entry['gene_name'] = feature_entry['gene_name']
159
+ if feature_entry['description'] != 'N/A':
160
+ merged_entry['description'] = feature_entry['description']
161
+ # If gene name is N/A, use the new description
162
+ if merged_entry['gene_name'] == 'N/A':
163
+ merged_entry['gene_name'] = feature_entry['description']
164
+
165
+ # Always update location information
166
+ merged_entry.update({
167
+ 'location': feature_entry['location'],
168
+ 'full_location': feature_entry['full_location'],
169
+ 'start': feature_entry['start'],
170
+ 'end': feature_entry['end']
171
+ })
172
+
173
+ index_data['locus_tags'][locus_tag] = merged_entry
174
+ else:
175
+ # New entry
176
+ index_data['locus_tags'][locus_tag] = feature_entry
177
 
178
  # Save compressed index to file
 
179
  with open(self.index_file, 'wb') as f:
180
  pickle.dump(index_data, f, protocol=pickle.HIGHEST_PROTOCOL)
 
 
 
181
  self._index = index_data
182
+
183
+ # if locus tag is CAALFM_C304810CA
184
+ if 'CAALFM_C304810CA' in index_data['locus_tags']:
185
+ print(f"Locus tag CAALFM_C304810CA found: {index_data['locus_tags']['CAALFM_C304810CA']}")
186
+
187
  self.logger.debug(f"Index creation complete. Records: {record_count}, Features: {feature_count}")
 
188
  return True
189
 
190
  except Exception as e:
 
192
  return False
193
 
194
  def _load_index(self):
 
195
  try:
196
+ self.logger.debug(f"Loading index from: {self.index_file}")
197
  if not os.path.exists(self.index_file):
198
  return False
199
+
200
  # Check if index is older than GenBank file
201
  if os.path.getmtime(self.index_file) < os.path.getmtime(self.annotation_file_name):
202
  return False
203
 
 
204
  with open(self.index_file, 'rb') as f:
205
  self._index = pickle.load(f)
 
 
 
206
  return True
207
 
208
  except Exception as e:
 
225
 
226
  # Search through index
227
  if hasattr(self, '_index') and 'locus_tags' in self._index:
 
228
  for locus_tag, feature_entry in self._index['locus_tags'].items():
 
 
 
 
 
 
 
 
229
  searchable_text = ' '.join([
230
  feature_entry.get('gene_name', '').lower(),
231
  locus_tag.lower(),
 
237
  info = {
238
  'feature_id': locus_tag,
239
  'feature_name': feature_entry.get('gene_name', 'N/A'),
240
+ 'feature_full_location': feature_entry.get('full_location', 'N/A'),
241
  'feature_location': feature_entry.get('location', 'N/A'),
242
+ 'feature_description': feature_entry.get('description', 'N/A'),
243
+ 'feature_type': feature_entry.get('feature_type', 'CDS')
244
  }
245
  results_list.append((feature_entry.get('chromosome', ''), info))
246
 
 
248
 
249
  except Exception as e:
250
  self.logger.error(f"Error in genbank_search: {str(e)}")
251
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
252
  raise
253
 
254
  def get_gene_data(self, gene_identifier):
 
275
  'feature_type': gene_info['feature_type'],
276
  'chromosome': gene_info['chromosome'],
277
  'location': gene_info['location'],
278
+ 'full_location': gene_info.get('full_location', ''), # Add full location
279
  'gene_name': gene_info['gene_name'],
280
  'description': gene_info['description'],
281
  'start': gene_info['start'],
 
298
  'feature_type': value['feature_type'],
299
  'chromosome': value['chromosome'],
300
  'location': value['location'],
301
+ 'full_location': value.get('full_location', ''), # Add full location
302
  'gene_name': value['gene_name'],
303
  'description': value['description'],
304
  'start': value['start'],
 
332
  padded_sequence = sequence[start:end]
333
 
334
  self.logger.debug(f"Padded sequence: {padded_sequence}")
 
335
  return padded_sequence
 
336
  return None
337
 
338
  except Exception as e:
339
  self.logger.error(f"Error getting sequence for gene: {str(e)}")
340
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/models/CSPRparser.py CHANGED
@@ -2,7 +2,6 @@ from utils.sequence_utils import SeqTranslate
2
  import logging
3
  from multiprocessing import Pool, cpu_count
4
  from functools import partial
5
- import time
6
  import pickle
7
  import os
8
  import traceback
@@ -16,9 +15,7 @@ class CSPRparser:
16
  self.index_file = f"{inputFileName}.index"
17
 
18
  def _create_index(self):
19
- """Create an index file for faster searching"""
20
  try:
21
- start_time = time.time()
22
  self.logger.debug("Creating CSPR index file...")
23
 
24
  # Initialize index structure
@@ -67,8 +64,6 @@ class CSPRparser:
67
 
68
  self._index = index_data
69
 
70
- create_time = time.time() - start_time
71
- self.logger.debug(f"Index creation time: {create_time:.2f} seconds")
72
  return True
73
 
74
  except Exception as e:
@@ -93,8 +88,9 @@ class CSPRparser:
93
 
94
  def read_targets_batch(self, chromosome, targets, endonuclease):
95
  try:
96
- start_time = time.time()
97
-
 
98
  # Load or create index
99
  if not hasattr(self, '_index'):
100
  if not self._load_index():
@@ -106,27 +102,20 @@ class CSPRparser:
106
  max_end = max(t['end'] for t in sorted_targets)
107
 
108
  self.logger.debug(f"Processing targets from {min_start} to {max_end}")
109
- self.logger.debug(f"Looking for chromosome number: {chromosome}")
110
 
111
  results = []
112
  lines_processed = 0
113
  lines_skipped = 0
114
 
115
- # Find chromosome in index by counting carets
116
  found_chrom = None
117
- chrom_count = 0
118
- target_chrom_num = int(chromosome) # Convert chromosome to integer
119
-
120
- # Debug available chromosomes
121
- self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
122
-
123
  for chrom_id in self._index:
124
  # Decode bytes to string if necessary
125
  chrom_str = chrom_id.decode() if isinstance(chrom_id, bytes) else chrom_id
126
 
127
- # Count carets ('>') to find the right chromosome
128
- chrom_count += 1
129
- if chrom_count == target_chrom_num:
130
  found_chrom = chrom_id
131
  self.logger.debug(f"Found matching chromosome: {chrom_str}")
132
  break
@@ -179,16 +168,13 @@ class CSPRparser:
179
  self.logger.error(f"Chromosome {chromosome} not found in index")
180
  self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
181
 
182
- total_time = time.time() - start_time
183
- self.logger.debug(f"Processed {lines_processed} lines, skipped {lines_skipped}")
184
- self.logger.debug(f"Found {len(results)} targets in {total_time:.2f} seconds")
185
-
186
  return results
187
 
188
  except Exception as e:
189
  self.logger.error(f"Error in read_targets_batch: {str(e)}")
190
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
191
  return []
 
192
  def parse_targets(self, file_path, region):
193
  """Parse targets with parallel processing and caching"""
194
  cache_key = f"{file_path}:{region}"
 
2
  import logging
3
  from multiprocessing import Pool, cpu_count
4
  from functools import partial
 
5
  import pickle
6
  import os
7
  import traceback
 
15
  self.index_file = f"{inputFileName}.index"
16
 
17
  def _create_index(self):
 
18
  try:
 
19
  self.logger.debug("Creating CSPR index file...")
20
 
21
  # Initialize index structure
 
64
 
65
  self._index = index_data
66
 
 
 
67
  return True
68
 
69
  except Exception as e:
 
88
 
89
  def read_targets_batch(self, chromosome, targets, endonuclease):
90
  try:
91
+
92
+
93
+ print(f"Reading targets for chromosome: {chromosome}")
94
  # Load or create index
95
  if not hasattr(self, '_index'):
96
  if not self._load_index():
 
102
  max_end = max(t['end'] for t in sorted_targets)
103
 
104
  self.logger.debug(f"Processing targets from {min_start} to {max_end}")
105
+ self.logger.debug(f"Looking for chromosome: {chromosome}")
106
 
107
  results = []
108
  lines_processed = 0
109
  lines_skipped = 0
110
 
111
+ # Find chromosome by full ID
112
  found_chrom = None
 
 
 
 
 
 
113
  for chrom_id in self._index:
114
  # Decode bytes to string if necessary
115
  chrom_str = chrom_id.decode() if isinstance(chrom_id, bytes) else chrom_id
116
 
117
+ # Match the full chromosome ID
118
+ if chrom_str == chromosome:
 
119
  found_chrom = chrom_id
120
  self.logger.debug(f"Found matching chromosome: {chrom_str}")
121
  break
 
168
  self.logger.error(f"Chromosome {chromosome} not found in index")
169
  self.logger.debug(f"Available chromosomes: {list(self._index.keys())}")
170
 
 
 
 
 
171
  return results
172
 
173
  except Exception as e:
174
  self.logger.error(f"Error in read_targets_batch: {str(e)}")
175
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
176
  return []
177
+
178
  def parse_targets(self, file_path, region):
179
  """Parse targets with parallel processing and caching"""
180
  cache_key = f"{file_path}:{region}"
src/models/DatabaseManager.py CHANGED
@@ -125,17 +125,34 @@ class DatabaseManager(QObject):
125
  return adjusted_path
126
 
127
  def _update_watched_directory(self):
128
- self.file_watcher.removePaths(self.file_watcher.directories())
129
-
130
- if self.db_path and os.path.isdir(self.db_path):
131
- self.file_watcher.addPath(self.db_path)
132
 
133
- # Also watch GBFF subdirectory if it exists
134
- gbff_path = os.path.join(self.db_path, 'GBFF')
135
- if os.path.isdir(gbff_path):
136
- self.file_watcher.addPath(gbff_path)
 
 
 
 
 
 
137
 
138
- self.logger.debug(f"Now watching directories: {self.file_watcher.directories()}")
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  def _detect_file_changes(self) -> Dict[FileChangeType, List[str]]:
141
  """Detect what files have changed and categorize the changes"""
@@ -173,25 +190,23 @@ class DatabaseManager(QObject):
173
  try:
174
  self.logger.debug(f"Detected change in directory: {path}")
175
 
 
 
 
176
  # Detect specific changes
177
  changes = self._detect_file_changes()
178
 
179
- if changes: # Only emit if there are actual changes
 
 
 
180
  self.logger.debug(f"Detected file changes: {changes}")
181
-
182
- # Get validation state
183
- is_valid, message = self.validate_db_path(path)
184
-
185
- # Emit separate signals
186
- self.db_validation_changed.emit(is_valid, message)
187
  self.db_files_changed.emit(changes)
188
 
189
- # Emit combined signal for components that want everything
190
- self.db_state_changed.emit(is_valid, message, changes)
191
-
192
- self.logger.info(f"Database state updated - Valid: {is_valid}, Changes: {changes}")
193
- else:
194
- self.logger.debug("No relevant file changes detected")
195
 
196
  except Exception as e:
197
  self.logger.error(f"Error handling directory change: {str(e)}")
 
125
  return adjusted_path
126
 
127
  def _update_watched_directory(self):
128
+ """Update the watched directory and validate the new path"""
129
+ try:
130
+ # Remove old watched directories
131
+ self.file_watcher.removePaths(self.file_watcher.directories())
132
 
133
+ if self.db_path and os.path.isdir(self.db_path):
134
+ # Add new directory to watch
135
+ self.file_watcher.addPath(self.db_path)
136
+
137
+ # Add GBFF subdirectory if it exists
138
+ gbff_path = os.path.join(self.db_path, 'GBFF')
139
+ if os.path.isdir(gbff_path):
140
+ self.file_watcher.addPath(gbff_path)
141
+
142
+ self.logger.debug(f"Now watching directories: {self.file_watcher.directories()}")
143
 
144
+ # Validate the new path and emit signals
145
+ is_valid, message = self.validate_db_path(self.db_path)
146
+ self.db_validation_changed.emit(is_valid, message)
147
+
148
+ # Also check for any file changes
149
+ changes = self._detect_file_changes()
150
+ if changes:
151
+ self.db_files_changed.emit(changes)
152
+ self.db_state_changed.emit(is_valid, message, changes)
153
+
154
+ except Exception as e:
155
+ self.logger.error(f"Error updating watched directory: {str(e)}")
156
 
157
  def _detect_file_changes(self) -> Dict[FileChangeType, List[str]]:
158
  """Detect what files have changed and categorize the changes"""
 
190
  try:
191
  self.logger.debug(f"Detected change in directory: {path}")
192
 
193
+ # Re-validate the path
194
+ is_valid, message = self.validate_db_path(self.db_path)
195
+
196
  # Detect specific changes
197
  changes = self._detect_file_changes()
198
 
199
+ # Always emit validation signal on directory change
200
+ self.db_validation_changed.emit(is_valid, message)
201
+
202
+ if changes: # Only emit change signals if there are actual changes
203
  self.logger.debug(f"Detected file changes: {changes}")
 
 
 
 
 
 
204
  self.db_files_changed.emit(changes)
205
 
206
+ # Always emit combined state signal
207
+ self.db_state_changed.emit(is_valid, message, changes or {})
208
+
209
+ self.logger.info(f"Database state updated - Valid: {is_valid}, Changes: {changes}")
 
 
210
 
211
  except Exception as e:
212
  self.logger.error(f"Error handling directory change: {str(e)}")
src/models/FindTargetsModel.py CHANGED
@@ -1,4 +1,3 @@
1
- import time
2
  from models.HomeWindowModel import HomeWindowModel
3
  from models.CSPRparser import CSPRparser
4
  from models.AnnotationParser import AnnotationParser
@@ -67,51 +66,60 @@ class FindTargetsModel(HomeWindowModel):
67
 
68
  def find_targets_by_feature(self, parser, input_data):
69
  try:
70
- annotation_file = (input_data.get('annotation_file') or
71
- self.global_settings.get_current_annotation_file())
72
 
73
- search_query = input_data['search_query'].strip()
 
 
74
 
75
- annotation_parser = AnnotationParser(self.global_settings)
76
- annotation_file_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
77
- annotation_parser.set_annotation_file(annotation_file_path)
 
 
 
78
 
79
- results_list = annotation_parser.genbank_search([search_query])
 
 
80
 
81
- formatted_results = []
82
 
83
- chrom_mapping = {}
84
- chrom_count = 0
85
- for record in SeqIO.parse(annotation_file_path, "genbank"):
86
- chrom_count += 1
87
- chrom_mapping[record.id] = str(chrom_count)
88
 
89
- for record_id, feature_info in results_list:
90
- location = feature_info['feature_location']
91
- start_end = location.split('(')[0] # Get part before the strand
92
- start, end = map(int, start_end.split(':'))
93
-
94
- chrom_num = chrom_mapping.get(record_id, '1')
95
-
96
- target_info = {
97
- 'feature_type': 'CDS',
98
- 'chromosome': chrom_num,
99
- 'full_chromosome': record_id,
100
- 'feature_id': feature_info['feature_id'],
101
- 'feature_name': feature_info['feature_name'],
102
- 'feature_description': feature_info['feature_description'],
103
- 'location': f"{start}-{end}",
104
- 'start': start,
105
- 'end': end,
106
- 'strand': '+' if '(+)' in location else '-',
107
- 'endonuclease': input_data['endonuclease']
108
- }
109
-
110
- self.global_settings.logger.debug(f"Created target info: {target_info}")
111
-
112
- formatted_results.append(target_info)
113
 
114
- return formatted_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  except Exception as e:
117
  self.global_settings.logger.error(f"Error in find_targets_by_feature: {str(e)}")
@@ -124,71 +132,66 @@ class FindTargetsModel(HomeWindowModel):
124
 
125
  for query in queries:
126
  try:
127
- chrom, start, end = map(int, query.strip().split(','))
128
-
129
- # Get full chromosome ID by counting carets
130
- full_chrom = None
131
- chrom_count = 0
132
 
133
  # Get annotation file path
134
  annotation_file = self.global_settings.get_current_annotation_file()
135
  annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
136
 
137
- # Find the full chromosome ID by position
 
 
 
138
  for record in SeqIO.parse(annotation_path, "genbank"):
139
- chrom_count += 1
140
- if chrom_count == chrom: # Match based on position
141
  full_chrom = record.id
142
- self.logger.debug(f"Found chromosome {chrom} as {full_chrom}")
143
  break
144
 
145
  if not full_chrom:
146
- self.logger.warning(f"Could not find chromosome at position {chrom}")
147
  continue
148
 
149
- # Create target info with proper formatting
150
- position_name = f"chrom {chrom}, start: {start}, end: {end}"
151
  target_info = [{
152
  'start': start,
153
  'end': end,
154
- 'feature_id': position_name,
155
  'feature_name': position_name,
156
- 'chromosome': str(chrom), # Keep chromosome number for CSPR lookup
157
- 'full_chromosome': full_chrom # Store full ID for sequence lookup
158
  }]
159
 
160
  # Get targets using batch processing
161
- self.logger.debug(f"Searching for targets in chromosome {chrom} from {start} to {end}")
162
- targets = parser.read_targets_batch(str(chrom), target_info, input_data['endonuclease'])
163
 
164
  if targets:
165
  self.logger.debug(f"Found {len(targets)} raw targets")
166
  filtered_targets = []
167
- guide_length = 23 # Length of guide RNA
168
 
169
  for target in targets:
170
  target_pos = int(target['position'])
171
- target_end = target_pos
172
 
173
- # Include target if:
174
- # 1. Target start position is within range
175
- # 2. Target end position is within or equal to end position
176
  if start <= target_pos and target_end <= end + 1:
177
  filtered_targets.append(target)
178
 
179
  self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
180
 
181
- # Get sequence for this region
182
- sequence = self._get_sequence_for_position(chrom, start, end)
183
-
184
  # Format results
185
  for target in filtered_targets:
186
  result = {
187
  'feature_type': 'Position',
188
- 'chromosome': str(chrom),
189
- 'feature_id': position_name,
190
  'feature_name': position_name,
191
- 'feature_description': position_name,
192
  'location': target['location'],
193
  'start': start,
194
  'end': end,
@@ -196,15 +199,10 @@ class FindTargetsModel(HomeWindowModel):
196
  'sequence': target['sequence'],
197
  'pam': target['pam'],
198
  'score': target['score'],
199
- 'endonuclease': target['endonuclease'],
200
- 'gene_sequence': sequence
201
  }
202
  all_results.append(result)
203
 
204
- self.logger.debug(f"Added {len(filtered_targets)} formatted results")
205
- else:
206
- self.logger.warning(f"No targets found for query: {query}")
207
-
208
  except Exception as e:
209
  self.logger.error(f"Error processing query {query}: {str(e)}")
210
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
@@ -261,11 +259,6 @@ class FindTargetsModel(HomeWindowModel):
261
  try:
262
  sequence = input_data['search_query'].strip().upper()
263
 
264
- # Validate sequence length
265
- if len(sequence) < 100:
266
- self.logger.error("Sequence too short")
267
- return []
268
-
269
  # Get annotation file
270
  annotation_file = self.global_settings.get_current_annotation_file()
271
  if not annotation_file:
@@ -279,9 +272,7 @@ class FindTargetsModel(HomeWindowModel):
279
  self.annotation_parser.set_annotation_file(annotation_path)
280
 
281
  # Find sequence in genome
282
- chrom_count = 0
283
  for record in SeqIO.parse(self.annotation_parser.annotation_file_name, "genbank"):
284
- chrom_count += 1 # Count chromosome position by caret
285
  record_seq = str(record.seq).upper()
286
  pos = record_seq.find(sequence)
287
 
@@ -290,8 +281,8 @@ class FindTargetsModel(HomeWindowModel):
290
  start = pos + 1 # 1-based position
291
  end = start + len(sequence) - 1
292
 
293
- # Create position name
294
- position_name = f"chrom {chrom_count}, start: {start}, end: {end}"
295
 
296
  # Create target info
297
  target_info = [{
@@ -299,13 +290,13 @@ class FindTargetsModel(HomeWindowModel):
299
  'end': end,
300
  'feature_id': position_name,
301
  'feature_name': position_name,
302
- 'chromosome': str(chrom_count), # Use caret-based chromosome number
303
- 'full_chromosome': record.id # Store full chromosome ID
304
  }]
305
 
306
  # Get targets in this region
307
- self.logger.debug(f"Found sequence in chromosome {chrom_count} from {start} to {end}")
308
- targets = parser.read_targets_batch(str(chrom_count), target_info, input_data['endonuclease'])
309
 
310
  if targets:
311
  self.logger.debug(f"Found {len(targets)} raw targets")
@@ -322,15 +313,12 @@ class FindTargetsModel(HomeWindowModel):
322
 
323
  self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
324
 
325
- # Get sequence with padding
326
- sequence_with_padding = self._get_sequence_for_position(chrom_count, start, end)
327
-
328
  # Format results
329
  all_results = []
330
  for target in filtered_targets:
331
  result = {
332
  'feature_type': 'Position',
333
- 'chromosome': str(chrom_count),
334
  'feature_id': position_name,
335
  'feature_name': position_name,
336
  'feature_description': f"Sequence match at {position_name}",
@@ -341,8 +329,7 @@ class FindTargetsModel(HomeWindowModel):
341
  'sequence': target['sequence'],
342
  'pam': target['pam'],
343
  'score': target['score'],
344
- 'endonuclease': target['endonuclease'],
345
- 'gene_sequence': sequence_with_padding
346
  }
347
  all_results.append(result)
348
 
 
 
1
  from models.HomeWindowModel import HomeWindowModel
2
  from models.CSPRparser import CSPRparser
3
  from models.AnnotationParser import AnnotationParser
 
66
 
67
  def find_targets_by_feature(self, parser, input_data):
68
  try:
69
+ # Get annotation file with proper validation
70
+ annotation_file = input_data.get('annotation_file')
71
 
72
+ if not annotation_file:
73
+ self.global_settings.logger.error("No annotation file selected")
74
+ raise ValueError("No annotation file selected. Please select an annotation file.")
75
 
76
+ # Construct proper path
77
+ annotation_file_path = os.path.normpath(os.path.join(
78
+ self.global_settings.get_db_path(),
79
+ 'GBFF',
80
+ annotation_file
81
+ ))
82
 
83
+ if not os.path.isfile(annotation_file_path):
84
+ self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
85
+ raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
86
 
87
+ self.global_settings.logger.debug(f"Using annotation file: {annotation_file_path}")
88
 
89
+ # Split search queries by newlines and remove empty lines
90
+ search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
 
 
 
91
 
92
+ annotation_parser = AnnotationParser(self.global_settings)
93
+ annotation_parser.set_annotation_file(annotation_file_path)
94
+
95
+ # Process each query and combine results
96
+ all_results = []
97
+ for search_query in search_queries:
98
+ results_list = annotation_parser.genbank_search([search_query])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ for record_id, feature_info in results_list:
101
+ location = feature_info['feature_location']
102
+ start_end = location.split('(')[0]
103
+ start, end = map(int, start_end.split(':'))
104
+
105
+ target_info = {
106
+ 'feature_type': feature_info['feature_type'],
107
+ 'chromosome': record_id,
108
+ 'feature_id': feature_info['feature_id'],
109
+ 'feature_name': feature_info['feature_name'],
110
+ 'feature_description': feature_info['feature_description'],
111
+ 'location': f"{start}-{end}",
112
+ 'full_location': feature_info.get('feature_full_location', ''),
113
+ 'start': start,
114
+ 'end': end,
115
+ 'strand': '+' if '(+)' in location else '-',
116
+ 'endonuclease': input_data['endonuclease'],
117
+ 'search_query': search_query
118
+ }
119
+
120
+ all_results.append(target_info)
121
+
122
+ return all_results
123
 
124
  except Exception as e:
125
  self.global_settings.logger.error(f"Error in find_targets_by_feature: {str(e)}")
 
132
 
133
  for query in queries:
134
  try:
135
+ # Parse the position query
136
+ chrom_pos, start, end = map(int, query.strip().split(','))
 
 
 
137
 
138
  # Get annotation file path
139
  annotation_file = self.global_settings.get_current_annotation_file()
140
  annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
141
 
142
+ # Find the chromosome by its position in the file
143
+ full_chrom = None
144
+ chromosome_count = 0
145
+
146
  for record in SeqIO.parse(annotation_path, "genbank"):
147
+ chromosome_count += 1
148
+ if chromosome_count == chrom_pos:
149
  full_chrom = record.id
150
+ self.logger.debug(f"Found chromosome at position {chrom_pos} with ID {full_chrom}")
151
  break
152
 
153
  if not full_chrom:
154
+ self.logger.warning(f"Could not find chromosome at position {chrom_pos}. Total chromosomes: {chromosome_count}")
155
  continue
156
 
157
+ # Create position name using full chromosome ID
158
+ position_name = f"chromosome {full_chrom}, start: {start}, end: {end}"
159
  target_info = [{
160
  'start': start,
161
  'end': end,
162
+ 'feature_id': position_name, # Use consistent format
163
  'feature_name': position_name,
164
+ 'chromosome': full_chrom, # Use raw chromosome ID
165
+ 'feature_type': 'Position' # Add feature type
166
  }]
167
 
168
  # Get targets using batch processing
169
+ self.logger.debug(f"Searching for targets in chromosome {full_chrom} from {start} to {end}")
170
+ targets = parser.read_targets_batch(full_chrom, target_info, input_data['endonuclease'])
171
 
172
  if targets:
173
  self.logger.debug(f"Found {len(targets)} raw targets")
174
  filtered_targets = []
175
+ guide_length = 23
176
 
177
  for target in targets:
178
  target_pos = int(target['position'])
179
+ target_end = target_pos + guide_length
180
 
181
+ # Include target if within sequence bounds
 
 
182
  if start <= target_pos and target_end <= end + 1:
183
  filtered_targets.append(target)
184
 
185
  self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
186
 
 
 
 
187
  # Format results
188
  for target in filtered_targets:
189
  result = {
190
  'feature_type': 'Position',
191
+ 'chromosome': full_chrom, # Use raw chromosome ID
192
+ 'feature_id': position_name, # Use consistent format
193
  'feature_name': position_name,
194
+ 'feature_description': f"Position match at {position_name}",
195
  'location': target['location'],
196
  'start': start,
197
  'end': end,
 
199
  'sequence': target['sequence'],
200
  'pam': target['pam'],
201
  'score': target['score'],
202
+ 'endonuclease': target['endonuclease']
 
203
  }
204
  all_results.append(result)
205
 
 
 
 
 
206
  except Exception as e:
207
  self.logger.error(f"Error processing query {query}: {str(e)}")
208
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
 
259
  try:
260
  sequence = input_data['search_query'].strip().upper()
261
 
 
 
 
 
 
262
  # Get annotation file
263
  annotation_file = self.global_settings.get_current_annotation_file()
264
  if not annotation_file:
 
272
  self.annotation_parser.set_annotation_file(annotation_path)
273
 
274
  # Find sequence in genome
 
275
  for record in SeqIO.parse(self.annotation_parser.annotation_file_name, "genbank"):
 
276
  record_seq = str(record.seq).upper()
277
  pos = record_seq.find(sequence)
278
 
 
281
  start = pos + 1 # 1-based position
282
  end = start + len(sequence) - 1
283
 
284
+ # Create position name using consistent format
285
+ position_name = f"chromosome {record.id}, start: {start}, end: {end}" # Match format used in position search
286
 
287
  # Create target info
288
  target_info = [{
 
290
  'end': end,
291
  'feature_id': position_name,
292
  'feature_name': position_name,
293
+ 'chromosome': record.id, # Use raw chromosome ID
294
+ 'feature_type': 'Position'
295
  }]
296
 
297
  # Get targets in this region
298
+ self.logger.debug(f"Found sequence in chromosome {record.id} from {start} to {end}")
299
+ targets = parser.read_targets_batch(record.id, target_info, input_data['endonuclease'])
300
 
301
  if targets:
302
  self.logger.debug(f"Found {len(targets)} raw targets")
 
313
 
314
  self.logger.debug(f"Filtered to {len(filtered_targets)} targets within range")
315
 
 
 
 
316
  # Format results
317
  all_results = []
318
  for target in filtered_targets:
319
  result = {
320
  'feature_type': 'Position',
321
+ 'chromosome': record.id,
322
  'feature_id': position_name,
323
  'feature_name': position_name,
324
  'feature_description': f"Sequence match at {position_name}",
 
329
  'sequence': target['sequence'],
330
  'pam': target['pam'],
331
  'score': target['score'],
332
+ 'endonuclease': target['endonuclease']
 
333
  }
334
  all_results.append(result)
335
 
src/models/GlobalSettings.py CHANGED
@@ -4,13 +4,52 @@ import sys
4
  import platform
5
  from functools import lru_cache
6
  import importlib
7
- from PyQt6.QtCore import QSettings, QObject, pyqtSignal
8
  from PyQt6.QtGui import QPalette, QColor
9
  from PyQt6.QtWidgets import QApplication
 
10
 
11
  from models.DatabaseManager import DatabaseManager, FileChangeType
12
  from models.ConfigManager import ConfigManager
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class GlobalSettings(QObject):
15
  first_time_startup = pyqtSignal()
16
  endonuclease_updated = pyqtSignal()
@@ -23,31 +62,91 @@ class GlobalSettings(QObject):
23
  self.app_dir_path = app_dir_path
24
  self.logger = self._setup_logging()
25
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
27
  self.config_manager.load_env()
28
 
29
  self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
30
 
 
31
  self._initialize_directories()
 
32
 
33
- self.db_manager = DatabaseManager(self.logger, self.config_manager)
34
-
35
- self.db_manager.db_files_changed.connect(self._on_db_files_changed)
36
- self.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
37
- self.db_manager.db_state_changed.connect(self._on_db_state_changed)
38
-
39
- self.CSPR_DB = self.db_manager.get_db_path()
40
- self.algorithms = self.config_manager.get_config_value('algorithms', ["Azimuth 2.0"])
41
-
42
- self.settings = QSettings("TrinhLab-UTK", "CASPER")
43
- self.theme = self.settings.value("theme", "light")
44
-
45
- self.light_palette = None
46
- self.dark_palette = None
47
- self.initialize_palettes()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- self.main_window = None
50
- self._current_annotation_file = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def _on_db_files_changed(self, changes):
53
  """Handle database file changes"""
@@ -171,25 +270,6 @@ class GlobalSettings(QObject):
171
  else:
172
  app.setPalette(self.light_palette)
173
 
174
- def initialize_palettes(self):
175
- self.light_palette = QPalette() # Use default Qt light palette
176
- self.dark_palette = QPalette()
177
-
178
- # Set up dark palette
179
- self.dark_palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
180
- self.dark_palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
181
- self.dark_palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
182
- self.dark_palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
183
- self.dark_palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(255, 255, 255))
184
- self.dark_palette.setColor(QPalette.ColorRole.ToolTipText, QColor(255, 255, 255))
185
- self.dark_palette.setColor(QPalette.ColorRole.Text, QColor(255, 255, 255))
186
- self.dark_palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
187
- self.dark_palette.setColor(QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
188
- self.dark_palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 0, 0))
189
- self.dark_palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
190
- self.dark_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
191
- self.dark_palette.setColor(QPalette.ColorRole.HighlightedText, QColor(0, 0, 0))
192
-
193
  def save_config(self):
194
  self.config_manager.save_config()
195
 
@@ -213,58 +293,43 @@ class GlobalSettings(QObject):
213
 
214
  @lru_cache(maxsize=None)
215
  def _get_window_class(self, window_name):
216
- """Get the controller class with better error handling and dynamic imports"""
217
  try:
218
- # Get the application root directory
219
- if hasattr(sys, 'frozen'):
220
- root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
221
- if platform.system() == 'Darwin': # macOS
222
- root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
223
- 'Contents', 'Resources', 'src')
224
  else:
225
- root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
226
-
227
- # Add root directory to Python path if not already there
228
- if root_dir not in sys.path:
229
- sys.path.insert(0, root_dir)
230
-
231
- # Import model (optional)
232
- try:
233
- model_name = f"{window_name}Model"
234
- model_file = os.path.join(root_dir, 'models', f"{model_name}.py")
 
 
 
235
 
236
- if os.path.exists(model_file):
237
- spec = importlib.util.spec_from_file_location(
238
- f"models.{model_name}",
239
- model_file
240
- )
241
- model_module = importlib.util.module_from_spec(spec)
242
- spec.loader.exec_module(model_module)
243
- sys.modules[f"models.{model_name}"] = model_module
244
- self.logger.debug(f"Successfully imported model from {model_file}")
245
- except Exception as e:
246
- self.logger.warning(f"Could not find model for {window_name}: {str(e)}")
247
-
248
- # Import controller (required)
249
- controller_name = f"{window_name}Controller"
250
- controller_file = os.path.join(root_dir, 'controllers', f"{controller_name}.py")
251
-
252
- if not os.path.exists(controller_file):
253
- raise ImportError(f"Controller file not found: {controller_file}")
254
 
255
- spec = importlib.util.spec_from_file_location(
256
- f"controllers.{controller_name}",
257
- controller_file
258
- )
259
- controller_module = importlib.util.module_from_spec(spec)
260
- spec.loader.exec_module(controller_module)
261
- sys.modules[f"controllers.{controller_name}"] = controller_module
262
 
263
  class_name = f"{window_name}Controller"
264
  if not hasattr(controller_module, class_name):
265
  raise AttributeError(f"Controller module does not contain class {class_name}")
266
 
267
- self.logger.debug(f"Successfully imported controller from {controller_file}")
268
  return getattr(controller_module, class_name)
269
 
270
  except Exception as e:
@@ -292,9 +357,21 @@ class GlobalSettings(QObject):
292
  return self._startup_window
293
 
294
  def get_home_window(self):
295
- controller = self._create_window("HomeWindow")
296
- self._current_home_window = controller
297
- return controller
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  def get_new_genome_window(self):
300
  controller = self._create_window("NewGenomeWindow")
@@ -311,10 +388,68 @@ class GlobalSettings(QObject):
311
  self._current_ncbi_window = controller
312
  return controller
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  def get_multitargeting_window(self):
315
- controller = self._create_window("MultitargetingWindow")
316
- self._current_multitargeting_window = controller
317
- return controller
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
  def get_population_analysis_window(self):
320
  controller = self._create_window("PopulationAnalysisWindow")
@@ -365,43 +500,36 @@ class GlobalSettings(QObject):
365
 
366
  def set_current_annotation_file(self, annotation_file):
367
  """Set the current annotation file and notify listeners"""
368
- if self._current_annotation_file != annotation_file:
369
- self._current_annotation_file = annotation_file
370
- self.logger.debug(f"Current annotation file changed to: {annotation_file}")
371
- self.annotation_file_changed.emit(annotation_file)
 
 
 
 
 
 
372
 
373
  def get_current_annotation_file(self):
374
  """Get the currently selected annotation file"""
375
- if not self._current_annotation_file and hasattr(self, '_current_home_window'):
376
- # Try to get from home window if not set
377
- self._current_annotation_file = self._current_home_window.get_annotation_file()
378
- return self._current_annotation_file
 
 
 
 
 
 
 
379
 
380
  def get_scoring_options_window(self, view_targets_controller):
381
  """Create and return ScoringOptionsController instance"""
382
  from controllers.ScoringOptionsController import ScoringOptionsController
383
  return ScoringOptionsController(self, view_targets_controller)
384
 
385
- def get_stylesheet(self):
386
- """Return the base stylesheet for the application"""
387
- # Implement this method to return a base stylesheet
388
- pass
389
-
390
- def get_groupbox_style(self):
391
- """Return the style for group boxes"""
392
- # Implement this method to return the group box style
393
- pass
394
-
395
- def get_dark_stylesheet(self):
396
- """Return the dark theme stylesheet"""
397
- # Implement this method to return the dark theme stylesheet
398
- pass
399
-
400
- def get_light_stylesheet(self):
401
- """Return the light theme stylesheet"""
402
- # Implement this method to return the light theme stylesheet
403
- pass
404
-
405
  def set_theme(self, theme):
406
  """Set the current theme and notify listeners"""
407
  self.theme = theme
@@ -425,5 +553,27 @@ class GlobalSettings(QObject):
425
  self._export_selected_grnas_controller = ExportSelectedgRNAsController(self)
426
  return self._export_selected_grnas_controller
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  # Global instance
429
  global_settings = None
 
4
  import platform
5
  from functools import lru_cache
6
  import importlib
7
+ from PyQt6.QtCore import QSettings, QObject, pyqtSignal, QThread
8
  from PyQt6.QtGui import QPalette, QColor
9
  from PyQt6.QtWidgets import QApplication
10
+ import time
11
 
12
  from models.DatabaseManager import DatabaseManager, FileChangeType
13
  from models.ConfigManager import ConfigManager
14
 
15
+ class ModulePreloader(QThread):
16
+ finished = pyqtSignal(str, object)
17
+
18
+ def __init__(self, global_settings, module_name):
19
+ super().__init__()
20
+ self.global_settings = global_settings
21
+ self.module_name = module_name
22
+ self.module = None # Store the loaded module
23
+
24
+ def run(self):
25
+ try:
26
+ module_path = f"controllers.{self.module_name}Controller"
27
+ if module_path not in self.global_settings._module_cache:
28
+ # Get root directory
29
+ if hasattr(sys, 'frozen'):
30
+ root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
31
+ if platform.system() == 'Darwin':
32
+ root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
33
+ 'Contents', 'Resources', 'src')
34
+ else:
35
+ root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36
+
37
+ if root_dir not in sys.path:
38
+ sys.path.insert(0, root_dir)
39
+
40
+ controller_file = os.path.join(root_dir, 'controllers', f"{self.module_name}Controller.py")
41
+
42
+ if os.path.exists(controller_file):
43
+ spec = importlib.util.spec_from_file_location(module_path, controller_file)
44
+ module = importlib.util.module_from_spec(spec)
45
+ spec.loader.exec_module(module)
46
+ sys.modules[module_path] = module
47
+ self.module = module
48
+ self.finished.emit(self.module_name, module)
49
+
50
+ except Exception as e:
51
+ self.global_settings.logger.error(f"Error preloading module {self.module_name}: {str(e)}")
52
+
53
  class GlobalSettings(QObject):
54
  first_time_startup = pyqtSignal()
55
  endonuclease_updated = pyqtSignal()
 
62
  self.app_dir_path = app_dir_path
63
  self.logger = self._setup_logging()
64
 
65
+ # Initialize important attributes
66
+ self._current_annotation_file = None
67
+ self._module_cache = {}
68
+ self._preloading_modules = {}
69
+ self.main_window = None
70
+
71
+ # Only preload essential controllers for startup
72
+ self._preload_essential_controllers()
73
+
74
+ # Start background loading of commonly used modules
75
+ self._background_load_common_modules()
76
+
77
  self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
78
  self.config_manager.load_env()
79
 
80
  self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
81
 
82
+ # Defer database initialization until needed
83
  self._initialize_directories()
84
+ self._init_db_manager()
85
 
86
+ # Defer theme initialization
87
+ self._init_theme_settings()
88
+
89
+ def _init_db_manager(self):
90
+ """Initialize database manager lazily"""
91
+ if not hasattr(self, 'db_manager'):
92
+ self.db_manager = DatabaseManager(self.logger, self.config_manager)
93
+ self.db_manager.db_files_changed.connect(self._on_db_files_changed)
94
+ self.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
95
+ self.db_manager.db_state_changed.connect(self._on_db_state_changed)
96
+ self.CSPR_DB = self.db_manager.get_db_path()
97
+ self.algorithms = self.config_manager.get_config_value('algorithms', ["Azimuth 2.0"])
98
+
99
+ def _init_theme_settings(self):
100
+ """Initialize theme settings lazily"""
101
+ if not hasattr(self, 'settings'):
102
+ self.settings = QSettings("TrinhLab-UTK", "CASPER")
103
+ self.theme = self.settings.value("theme", "light")
104
+ self.light_palette = None
105
+ self.dark_palette = None
106
+
107
+ def _preload_essential_controllers(self):
108
+ """Preload only the essential controllers needed for startup"""
109
+ try:
110
+ essential_controllers = [
111
+ "StartupWindow",
112
+ "HomeWindow"
113
+ ] if self.is_first_time_startup else ["HomeWindow"]
114
+
115
+ for controller_name in essential_controllers:
116
+ self._preload_controller(controller_name)
117
+
118
+ except Exception as e:
119
+ self.logger.warning(f"Essential controller preloading failed: {str(e)}")
120
 
121
+ def _preload_controller(self, window_name):
122
+ """Preload a single controller with optimized imports"""
123
+ try:
124
+ module_path = f"controllers.{window_name}Controller"
125
+ if module_path not in self._module_cache:
126
+ # Get root directory only once
127
+ if not hasattr(self, '_root_dir'):
128
+ if hasattr(sys, 'frozen'):
129
+ self._root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
130
+ if platform.system() == 'Darwin':
131
+ self._root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
132
+ 'Contents', 'Resources', 'src')
133
+ else:
134
+ self._root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
135
+
136
+ if self._root_dir not in sys.path:
137
+ sys.path.insert(0, self._root_dir)
138
+
139
+ controller_file = os.path.join(self._root_dir, 'controllers', f"{window_name}Controller.py")
140
+
141
+ if os.path.exists(controller_file):
142
+ spec = importlib.util.spec_from_file_location(module_path, controller_file)
143
+ module = importlib.util.module_from_spec(spec)
144
+ spec.loader.exec_module(module)
145
+ sys.modules[module_path] = module
146
+ self._module_cache[module_path] = module
147
+
148
+ except Exception as e:
149
+ self.logger.warning(f"Failed to preload controller {window_name}: {str(e)}")
150
 
151
  def _on_db_files_changed(self, changes):
152
  """Handle database file changes"""
 
270
  else:
271
  app.setPalette(self.light_palette)
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  def save_config(self):
274
  self.config_manager.save_config()
275
 
 
293
 
294
  @lru_cache(maxsize=None)
295
  def _get_window_class(self, window_name):
296
+ """Get the controller class with optimized loading"""
297
  try:
298
+ start_time = time.time()
299
+
300
+ # Check if module is already cached
301
+ module_path = f"controllers.{window_name}Controller"
302
+ if module_path in self._module_cache:
303
+ controller_module = self._module_cache[module_path]
304
  else:
305
+ # Fall back to regular import if not cached
306
+ if hasattr(sys, 'frozen'):
307
+ root_dir = os.path.join(os.path.dirname(sys.executable), 'src')
308
+ if platform.system() == 'Darwin':
309
+ root_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(sys.executable))),
310
+ 'Contents', 'Resources', 'src')
311
+ else:
312
+ root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
313
+
314
+ if root_dir not in sys.path:
315
+ sys.path.insert(0, root_dir)
316
+
317
+ controller_file = os.path.join(root_dir, 'controllers', f"{window_name}Controller.py")
318
 
319
+ if not os.path.exists(controller_file):
320
+ raise ImportError(f"Controller file not found: {controller_file}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ spec = importlib.util.spec_from_file_location(module_path, controller_file)
323
+ controller_module = importlib.util.module_from_spec(spec)
324
+ spec.loader.exec_module(controller_module)
325
+ sys.modules[module_path] = controller_module
326
+ self._module_cache[module_path] = controller_module
 
 
327
 
328
  class_name = f"{window_name}Controller"
329
  if not hasattr(controller_module, class_name):
330
  raise AttributeError(f"Controller module does not contain class {class_name}")
331
 
332
+ self.logger.debug(f"Window class retrieval took: {time.time() - start_time:.2f} seconds")
333
  return getattr(controller_module, class_name)
334
 
335
  except Exception as e:
 
357
  return self._startup_window
358
 
359
  def get_home_window(self):
360
+ """Get or create home window with proper initialization"""
361
+ try:
362
+ controller = self._create_window("HomeWindow")
363
+ self._current_home_window = controller
364
+
365
+ # Initialize annotation file if needed
366
+ if not hasattr(self, '_current_annotation_file'):
367
+ self._current_annotation_file = None
368
+ if hasattr(controller, 'view'):
369
+ self._current_annotation_file = controller.view.get_annotation_file()
370
+
371
+ return controller
372
+ except Exception as e:
373
+ self.logger.error(f"Error creating home window: {str(e)}")
374
+ raise
375
 
376
  def get_new_genome_window(self):
377
  controller = self._create_window("NewGenomeWindow")
 
388
  self._current_ncbi_window = controller
389
  return controller
390
 
391
+ def _background_load_common_modules(self):
392
+ """Start background loading of commonly used modules"""
393
+ try:
394
+ common_modules = ["MultitargetingWindow", "PopulationAnalysisWindow"]
395
+ for module_name in common_modules:
396
+ if (module_name not in self._module_cache and
397
+ module_name not in self._preloading_modules):
398
+ preloader = ModulePreloader(self, module_name)
399
+ preloader.finished.connect(self._on_module_preloaded)
400
+ self._preloading_modules[module_name] = preloader
401
+ preloader.start()
402
+ except Exception as e:
403
+ self.logger.warning(f"Error starting background module loading: {str(e)}")
404
+
405
+ def _on_module_preloaded(self, module_name, module):
406
+ """Handle completion of module preloading"""
407
+ try:
408
+ module_path = f"controllers.{module_name}Controller"
409
+ self._module_cache[module_path] = module
410
+ if module_name in self._preloading_modules:
411
+ preloader = self._preloading_modules[module_name]
412
+ if not preloader.isRunning(): # Only remove if thread is finished
413
+ del self._preloading_modules[module_name]
414
+ self.logger.debug(f"Module {module_name} preloaded successfully")
415
+ except Exception as e:
416
+ self.logger.error(f"Error handling preloaded module: {str(e)}")
417
+
418
  def get_multitargeting_window(self):
419
+ """Create and return MultitargetingController instance with optimized loading"""
420
+ try:
421
+ start_time = time.time()
422
+ self.logger.debug("Starting multitargeting window creation")
423
+
424
+ # Check if module is being preloaded
425
+ if "MultitargetingWindow" in self._preloading_modules:
426
+ preloader = self._preloading_modules["MultitargetingWindow"]
427
+ if preloader.isRunning():
428
+ self.logger.debug("Waiting for preloader to complete...")
429
+ preloader.wait()
430
+ if preloader.module: # Use the stored module
431
+ WindowClass = getattr(preloader.module, "MultitargetingWindowController")
432
+ else:
433
+ WindowClass = self._get_window_class("MultitargetingWindow")
434
+ else:
435
+ WindowClass = self._get_window_class("MultitargetingWindow")
436
+ else:
437
+ WindowClass = self._get_window_class("MultitargetingWindow")
438
+
439
+ # Create controller instance
440
+ controller_start = time.time()
441
+ controller = WindowClass(self)
442
+ self.logger.debug(f"Controller instantiation took: {time.time() - controller_start:.2f} seconds")
443
+
444
+ # Store the reference
445
+ self._current_multitargeting_window = controller
446
+
447
+ self.logger.debug(f"Total multitargeting window creation took: {time.time() - start_time:.2f} seconds")
448
+ return controller
449
+
450
+ except Exception as e:
451
+ self.logger.error(f"Error creating multitargeting window: {str(e)}")
452
+ raise
453
 
454
  def get_population_analysis_window(self):
455
  controller = self._create_window("PopulationAnalysisWindow")
 
500
 
501
  def set_current_annotation_file(self, annotation_file):
502
  """Set the current annotation file and notify listeners"""
503
+ try:
504
+ if not hasattr(self, '_current_annotation_file'):
505
+ self._current_annotation_file = None
506
+
507
+ if self._current_annotation_file != annotation_file:
508
+ self._current_annotation_file = annotation_file
509
+ self.logger.debug(f"Current annotation file changed to: {annotation_file}")
510
+ self.annotation_file_changed.emit(annotation_file)
511
+ except Exception as e:
512
+ self.logger.error(f"Error setting current annotation file: {str(e)}")
513
 
514
  def get_current_annotation_file(self):
515
  """Get the currently selected annotation file"""
516
+ try:
517
+ if not self._current_annotation_file and hasattr(self, '_current_home_window'):
518
+ # Try to get from home window if not set
519
+ home_controller = self._current_home_window
520
+ if hasattr(home_controller, 'view'):
521
+ self._current_annotation_file = home_controller.view.get_annotation_file()
522
+ self.logger.debug(f"Got annotation file from home window: {self._current_annotation_file}")
523
+ return self._current_annotation_file
524
+ except Exception as e:
525
+ self.logger.error(f"Error getting current annotation file: {str(e)}")
526
+ return None
527
 
528
  def get_scoring_options_window(self, view_targets_controller):
529
  """Create and return ScoringOptionsController instance"""
530
  from controllers.ScoringOptionsController import ScoringOptionsController
531
  return ScoringOptionsController(self, view_targets_controller)
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  def set_theme(self, theme):
534
  """Set the current theme and notify listeners"""
535
  self.theme = theme
 
553
  self._export_selected_grnas_controller = ExportSelectedgRNAsController(self)
554
  return self._export_selected_grnas_controller
555
 
556
+ def adjust_path_for_os(self, path):
557
+ """
558
+ Adjust file path based on operating system
559
+ """
560
+ try:
561
+ # Convert path separators to match the current OS
562
+ adjusted_path = os.path.normpath(path)
563
+
564
+ # For Windows, ensure the path uses backslashes
565
+ if platform.system() == 'Windows':
566
+ adjusted_path = adjusted_path.replace('/', '\\')
567
+ # For Unix-like systems (Linux, macOS), ensure the path uses forward slashes
568
+ else:
569
+ adjusted_path = adjusted_path.replace('\\', '/')
570
+
571
+ self.logger.debug(f"Adjusted path from '{path}' to '{adjusted_path}'")
572
+ return adjusted_path
573
+
574
+ except Exception as e:
575
+ self.logger.error(f"Error adjusting path: {str(e)}")
576
+ return path # Return original path if adjustment fails
577
+
578
  # Global instance
579
  global_settings = None
src/models/HomeWindowModel.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  import glob
3
- from typing import Dict, List, Set
4
  from utils.ui import show_error
5
  from models.DatabaseManager import FileChangeType
6
 
 
1
  import os
2
  import glob
3
+ from typing import Dict, List
4
  from utils.ui import show_error
5
  from models.DatabaseManager import FileChangeType
6
 
src/models/MultitargetingWindowModel.py CHANGED
@@ -1,9 +1,12 @@
1
  import os
2
  import sqlite3
3
  import statistics
 
 
4
 
5
  class MultitargetingWindowModel:
6
  def __init__(self, global_settings):
 
7
  self.settings = global_settings
8
  self.logger = global_settings.get_logger()
9
 
@@ -12,14 +15,21 @@ class MultitargetingWindowModel:
12
  self.row_limit = 1000
13
 
14
  # Get organism and endo mappings from DatabaseManager
 
15
  self.organisms_to_files, self.organisms_to_endos = self.settings.db_manager.get_organisms_and_endos()
 
 
 
 
16
 
 
17
  def get_organisms(self):
18
- """Get list of available organisms"""
19
  return list(self.organisms_to_endos.keys())
20
 
 
21
  def get_endos_for_organism(self, organism):
22
- """Get available endonucleases for given organism"""
23
  return self.organisms_to_endos.get(organism, [])
24
 
25
  def set_files(self, organism, endo):
@@ -36,6 +46,7 @@ class MultitargetingWindowModel:
36
 
37
  def get_repeats_data(self):
38
  """Get repeats data for the seeds table"""
 
39
  if not self.db_file:
40
  raise ValueError("Database file not set. Please select an organism and endonuclease first.")
41
 
@@ -43,6 +54,7 @@ class MultitargetingWindowModel:
43
  conn = sqlite3.connect(self.db_file)
44
  c = conn.cursor()
45
 
 
46
  # Use row limit in query
47
  if self.row_limit == -1: # No limit
48
  query = "SELECT * FROM repeats ORDER BY count DESC;"
@@ -107,9 +119,13 @@ class MultitargetingWindowModel:
107
  pams[majority_index], # PAM
108
  strand # Strand
109
  ))
110
-
 
 
111
  c.close()
112
  conn.close()
 
 
113
  return results
114
 
115
  except Exception as e:
@@ -280,3 +296,9 @@ class MultitargetingWindowModel:
280
  def get_row_limit(self):
281
  """Get current row limit setting"""
282
  return self.row_limit
 
 
 
 
 
 
 
1
  import os
2
  import sqlite3
3
  import statistics
4
+ from functools import lru_cache
5
+ import time
6
 
7
  class MultitargetingWindowModel:
8
  def __init__(self, global_settings):
9
+ start_time = time.time()
10
  self.settings = global_settings
11
  self.logger = global_settings.get_logger()
12
 
 
15
  self.row_limit = 1000
16
 
17
  # Get organism and endo mappings from DatabaseManager
18
+ db_start = time.time()
19
  self.organisms_to_files, self.organisms_to_endos = self.settings.db_manager.get_organisms_and_endos()
20
+ self.logger.debug(f"Getting DB mappings took: {time.time() - db_start:.2f} seconds")
21
+
22
+ self._cache = {}
23
+ self.logger.debug(f"Model initialization took: {time.time() - start_time:.2f} seconds")
24
 
25
+ @lru_cache(maxsize=32)
26
  def get_organisms(self):
27
+ """Get list of available organisms with caching"""
28
  return list(self.organisms_to_endos.keys())
29
 
30
+ @lru_cache(maxsize=32)
31
  def get_endos_for_organism(self, organism):
32
+ """Get available endonucleases for given organism with caching"""
33
  return self.organisms_to_endos.get(organism, [])
34
 
35
  def set_files(self, organism, endo):
 
46
 
47
  def get_repeats_data(self):
48
  """Get repeats data for the seeds table"""
49
+ start_time = time.time()
50
  if not self.db_file:
51
  raise ValueError("Database file not set. Please select an organism and endonuclease first.")
52
 
 
54
  conn = sqlite3.connect(self.db_file)
55
  c = conn.cursor()
56
 
57
+ query_start = time.time()
58
  # Use row limit in query
59
  if self.row_limit == -1: # No limit
60
  query = "SELECT * FROM repeats ORDER BY count DESC;"
 
119
  pams[majority_index], # PAM
120
  strand # Strand
121
  ))
122
+
123
+ self.logger.debug(f"Query and processing took: {time.time() - query_start:.2f} seconds")
124
+
125
  c.close()
126
  conn.close()
127
+
128
+ self.logger.debug(f"Total get_repeats_data took: {time.time() - start_time:.2f} seconds")
129
  return results
130
 
131
  except Exception as e:
 
296
  def get_row_limit(self):
297
  """Get current row limit setting"""
298
  return self.row_limit
299
+
300
+ def _clear_cache(self):
301
+ """Clear the internal cache"""
302
+ self._cache.clear()
303
+ self.get_organisms.cache_clear()
304
+ self.get_endos_for_organism.cache_clear()
src/models/NCBIWindowModel.py CHANGED
@@ -1,42 +1,231 @@
1
  import pandas as pd
2
  from PyQt6 import QtCore, QtGui
3
  from Bio import Entrez
4
- from bs4 import BeautifulSoup, XMLParsedAsHTMLWarning
5
  from ftplib import FTP
 
6
  import gzip
7
  import os
8
- import ssl
9
  import platform
10
- import warnings
11
- import xml.etree.ElementTree as ET
12
- from PyQt6.QtCore import Qt
13
- import socket
14
  from urllib.parse import urlparse
15
 
16
  class NCBIWindowModel:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  def __init__(self, settings):
18
  self.settings = settings
19
  self.logger = settings.get_logger()
20
  self.df = pd.DataFrame()
21
  self.genbank_ftp_dict = {}
22
  self.refseq_ftp_dict = {}
23
- self.files = [] # Initialize the files list
24
- Entrez.email = "your_email@example.com" # Replace with a valid email
 
 
 
 
25
 
26
- # Make DownloadThread accessible through the model
27
- self.DownloadThread = DownloadThread
28
-
29
- # Suppress the XMLParsedAsHTMLWarning
30
- warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
31
 
32
  def search_ncbi(self, search_params):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  try:
34
  retmax = int(search_params['max_results']) if search_params['max_results'] else 100
35
  term = f'"{search_params["organism"]}"[Organism]'
36
 
37
  if search_params['complete_genomes_only']:
38
  term += ' AND "Complete Genome"[Assembly Level]'
39
- if search_params['strain']:
40
  term += f' AND "{search_params["strain"]}"[Infraspecific name]'
41
 
42
  self.logger.info(f"Searching NCBI with term: {term}")
@@ -72,27 +261,176 @@ class NCBIWindowModel:
72
  self.logger.error(f"Error in query_ncbi: {str(e)}", exc_info=True)
73
  raise
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def _process_ncbi_data(self, summary_results):
76
  data = []
77
  for assembly in summary_results['DocumentSummarySet']['DocumentSummary']:
78
  gb_ftp = assembly.get('FtpPath_GenBank', '')
79
  rs_ftp = assembly.get('FtpPath_RefSeq', '')
80
 
 
 
 
 
 
 
 
 
 
 
 
81
  entry = {
82
- 'ID': assembly.attributes['uid'],
83
  'Species Name': assembly.get('SpeciesName', 'N/A'),
84
- 'Strain': assembly.get('Biosource', {}).get('InfraspeciesList', [{}])[0].get('Sub_value', 'N/A'),
85
  'Assembly Name': assembly.get('AssemblyName', 'N/A'),
86
- 'RefSeq assembly accession': 'Not Available' if not rs_ftp else assembly.get('AssemblyAccession', 'N/A'),
87
- 'GenBank assembly accession': 'Not Available' if not gb_ftp else assembly.get('AssemblyAccession', 'N/A'),
88
  'Assembly Status': assembly.get('AssemblyStatus', 'N/A')
89
  }
90
 
 
91
  if gb_ftp:
92
- self.genbank_ftp_dict[entry['ID']] = gb_ftp + '/'
 
 
93
  if rs_ftp:
94
- self.refseq_ftp_dict[entry['ID']] = rs_ftp + '/'
95
-
 
96
  data.append(entry)
97
 
98
  self.df = pd.DataFrame(data)
@@ -102,20 +440,176 @@ class NCBIWindowModel:
102
  self.logger.warning("No data found in NCBI response")
103
  else:
104
  self.logger.info(f"Processed {len(self.df)} entries from NCBI response")
105
-
106
- # Log a sample of the processed data
107
- if not self.df.empty:
108
- self.logger.debug(f"Sample of processed data:\n{self.df.head().to_string()}")
109
 
110
  def _get_element_text(self, element, tag):
111
  el = element.find(tag)
112
  return el.text if el is not None else 'N/A'
113
 
114
  def get_download_url(self, id, use_genbank):
115
- url = self.genbank_ftp_dict.get(id, '') if use_genbank else self.refseq_ftp_dict.get(id, '')
116
- if url and not url.startswith('ftp://'):
117
- url = 'ftp://' + url
118
- return url.rstrip('/') # Remove trailing slash if present
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  def decompress_file(self, filename):
121
  try:
@@ -134,10 +628,9 @@ class NCBIWindowModel:
134
  raise
135
 
136
  def get_output_path(self, file_type):
137
- if platform.system() == "Windows":
138
- return os.path.join(self.settings.CSPR_DB, file_type)
139
- else:
140
- return os.path.join(self.settings.CSPR_DB, file_type)
141
 
142
  def rename_file(self, old_name, new_name, file_type):
143
  old_path = os.path.join(self.get_output_path(file_type), old_name)
@@ -235,118 +728,3 @@ class CustomProxyModel(QtCore.QSortFilterProxyModel):
235
  if regex.match(text).hasMatch():
236
  return False
237
  return True
238
-
239
- class DownloadThread(QtCore.QThread):
240
- finished = QtCore.pyqtSignal(bool)
241
- progress_updated = QtCore.pyqtSignal(int, int, int)
242
- status_updated = QtCore.pyqtSignal(str)
243
- all_completed = QtCore.pyqtSignal()
244
-
245
- def __init__(self, controller, url, id, species_name, strain, download_fna, download_gbff):
246
- super().__init__()
247
- self.controller = controller
248
- self.url = url
249
- self.id = id
250
- self.species_name = species_name
251
- self.strain = strain
252
- self.download_fna = download_fna
253
- self.download_gbff = download_gbff
254
-
255
- def run(self):
256
- try:
257
- parsed_url = urlparse(self.url)
258
- ftp_host = parsed_url.netloc
259
- ftp_path = parsed_url.path
260
-
261
- self.controller.logger.info(f"Attempting to connect to FTP server: {ftp_host}")
262
-
263
- try:
264
- ip_address = socket.gethostbyname(ftp_host)
265
- self.controller.logger.info(f"Resolved IP address: {ip_address}")
266
- except socket.gaierror as e:
267
- self.controller.logger.error(f"Failed to resolve hostname: {ftp_host}. Error: {str(e)}")
268
- self.finished.emit(False)
269
- return
270
-
271
- ftp = FTP(ftp_host)
272
- ftp.login()
273
- ftp.cwd(ftp_path)
274
- ftp.set_pasv(True)
275
-
276
- # Set binary mode before any operations
277
- ftp.voidcmd('TYPE I')
278
-
279
- files_to_download = []
280
-
281
- # Get list of all files
282
- all_files = ftp.nlst()
283
-
284
- # Process FNA files if requested
285
- if self.download_fna:
286
- # Find the main genomic FNA file (should be exactly one)
287
- genomic_fna = [f for f in all_files
288
- if f.endswith('_genomic.fna.gz')
289
- and not any(x in f for x in ['cds_from', 'rna_from'])]
290
-
291
- if genomic_fna:
292
- files_to_download.append(genomic_fna[0])
293
- self.controller.logger.info(f"Found main genomic FNA file: {genomic_fna[0]}")
294
-
295
- # Process GBFF files if requested
296
- if self.download_gbff:
297
- gbff_files = [f for f in all_files if f.endswith('_genomic.gbff.gz')]
298
- files_to_download.extend(gbff_files)
299
- self.controller.logger.info(f"Found GBFF files: {gbff_files}")
300
-
301
- # Calculate total size with error handling
302
- total_size = 0
303
- for file in files_to_download:
304
- try:
305
- size = ftp.size(file)
306
- if size is not None:
307
- total_size += size
308
- except Exception as e:
309
- self.controller.logger.warning(f"Could not get size for file {file}: {str(e)}")
310
-
311
- downloaded_size = 0
312
-
313
- # Download files
314
- for file in files_to_download:
315
- try:
316
- self.status_updated.emit(f"Downloading: {file}")
317
- file_type = 'FNA' if file.endswith('.fna.gz') else 'GBFF'
318
- output_dir = os.path.join(self.controller.settings.CSPR_DB, file_type)
319
- os.makedirs(output_dir, exist_ok=True)
320
-
321
- local_filename = os.path.join(output_dir, file)
322
- self.controller.logger.info(f"Downloading file: {file} to {local_filename}")
323
-
324
- with open(local_filename, 'wb') as local_file:
325
- def callback(data):
326
- local_file.write(data)
327
- nonlocal downloaded_size
328
- downloaded_size += len(data)
329
- if total_size > 0:
330
- self.progress_updated.emit(self.id, downloaded_size, total_size)
331
-
332
- ftp.retrbinary(f"RETR {file}", callback)
333
-
334
- self.controller.logger.info(f"Download complete: {file}")
335
- self.status_updated.emit(f"Decompressing: {file}")
336
-
337
- self.controller.model.decompress_file(local_filename)
338
- decompressed_filename = local_filename[:-3]
339
- self.controller.model.add_downloaded_file(decompressed_filename)
340
-
341
- except Exception as e:
342
- self.controller.logger.error(f"Error downloading file {file}: {str(e)}")
343
- continue
344
-
345
- ftp.quit()
346
- self.controller.logger.info(f"All files downloaded and decompressed successfully for ID: {self.id}")
347
- self.all_completed.emit()
348
- self.finished.emit(True)
349
-
350
- except Exception as e:
351
- self.controller.logger.error(f"Download error for ID {self.id}: {str(e)}", exc_info=True)
352
- self.finished.emit(False)
 
1
  import pandas as pd
2
  from PyQt6 import QtCore, QtGui
3
  from Bio import Entrez
 
4
  from ftplib import FTP
5
+ from PyQt6.QtCore import Qt
6
  import gzip
7
  import os
 
8
  import platform
9
+ import requests
 
 
 
10
  from urllib.parse import urlparse
11
 
12
  class NCBIWindowModel:
13
+ class DownloadThread(QtCore.QThread):
14
+ finished = QtCore.pyqtSignal(bool)
15
+ progress_updated = QtCore.pyqtSignal(int, int, int)
16
+ status_updated = QtCore.pyqtSignal(str)
17
+ all_completed = QtCore.pyqtSignal()
18
+
19
+ def __init__(self, controller, url, id, species_name, strain, download_fna, download_gbff):
20
+ super().__init__()
21
+ self.controller = controller
22
+ self.url = url
23
+ self.id = id
24
+ self.species_name = species_name
25
+ self.strain = strain
26
+ self.download_fna = download_fna
27
+ self.download_gbff = download_gbff
28
+ self.logger = controller.settings.get_logger()
29
+ self.db_path = controller.settings.get_db_path()
30
+
31
+ def run(self):
32
+ try:
33
+ parsed_url = urlparse(self.url)
34
+ if parsed_url.scheme == 'ftp':
35
+ self._download_ftp()
36
+ else:
37
+ self._download_http()
38
+ self.all_completed.emit()
39
+ self.finished.emit(True)
40
+ except Exception as e:
41
+ self.logger.error(f"Error in download thread: {str(e)}")
42
+ self.finished.emit(False)
43
+
44
+ def _download_http(self):
45
+ try:
46
+ self.status_updated.emit(f"Downloading {self.species_name} ({self.strain})")
47
+ response = requests.get(self.url, stream=True)
48
+ response.raise_for_status()
49
+
50
+ total_size = int(response.headers.get('content-length', 0))
51
+ is_gzipped = self.url.endswith('.gz') or 'gzip=true' in self.url
52
+ is_fna = '.fna.' in self.url.lower() or 'fasta' in self.url.lower()
53
+
54
+ file_type = 'FNA' if is_fna else 'GBFF'
55
+ extension = '.gz' if is_gzipped else ''
56
+
57
+ # Create output directory using the current database path
58
+ output_dir = os.path.join(self.db_path, file_type)
59
+ os.makedirs(output_dir, exist_ok=True)
60
+
61
+ # Determine correct file extension and name
62
+ if is_fna:
63
+ local_filename = os.path.join(output_dir, f"{self.id}.fna{extension}")
64
+ else:
65
+ local_filename = os.path.join(output_dir, f"{self.id}.gbff{extension}")
66
+
67
+ downloaded_size = 0
68
+ with open(local_filename, 'wb') as f:
69
+ for chunk in response.iter_content(chunk_size=8192):
70
+ if chunk:
71
+ f.write(chunk)
72
+ downloaded_size += len(chunk)
73
+ if total_size:
74
+ progress = (downloaded_size / total_size) * 100
75
+ self.progress_updated.emit(self.id, progress, 100)
76
+
77
+ if not os.path.exists(local_filename) or os.path.getsize(local_filename) == 0:
78
+ raise Exception(f"Downloaded file {local_filename} is empty or does not exist")
79
+
80
+ if is_gzipped:
81
+ try:
82
+ # Read the gzipped content and write to uncompressed file
83
+ uncompressed_filename = local_filename[:-3] # Remove .gz
84
+ with gzip.open(local_filename, 'rb') as f_in:
85
+ content = f_in.read()
86
+ if not content:
87
+ raise Exception("Decompressed content is empty")
88
+ with open(uncompressed_filename, 'wb') as f_out:
89
+ f_out.write(content)
90
+
91
+ # Verify uncompressed file
92
+ if not os.path.exists(uncompressed_filename) or os.path.getsize(uncompressed_filename) == 0:
93
+ raise Exception(f"Decompressed file is empty")
94
+
95
+ # Remove the gzipped file only if decompression was successful
96
+ os.remove(local_filename)
97
+ self.controller.model.add_downloaded_file(uncompressed_filename)
98
+
99
+ except Exception as e:
100
+ self.logger.error(f"Error decompressing file: {str(e)}")
101
+ # Try to handle the file as non-gzipped if decompression fails
102
+ if os.path.exists(local_filename) and os.path.getsize(local_filename) > 0:
103
+ uncompressed_filename = local_filename[:-3]
104
+ os.rename(local_filename, uncompressed_filename)
105
+ self.controller.model.add_downloaded_file(uncompressed_filename)
106
+ else:
107
+ raise
108
+ else:
109
+ self.controller.model.add_downloaded_file(local_filename)
110
+
111
+ self.status_updated.emit(f"Download complete: {self.species_name}")
112
+
113
+ except Exception as e:
114
+ self.logger.error(f"Error in HTTP download: {str(e)}")
115
+ raise
116
+
117
+ def _download_ftp(self):
118
+ try:
119
+ self.status_updated.emit(f"Downloading {self.species_name} ({self.strain})")
120
+
121
+ parsed_url = urlparse(self.url)
122
+ ftp = FTP(parsed_url.netloc)
123
+ ftp.login()
124
+
125
+ file_size = ftp.size(parsed_url.path[1:])
126
+ is_gzipped = self.url.endswith('.gz')
127
+ is_fna = '.fna.' in self.url.lower() or '.fasta.' in self.url.lower()
128
+
129
+ file_type = 'FNA' if is_fna else 'GBFF'
130
+ extension = '.gz' if is_gzipped else ''
131
+
132
+ local_filename = os.path.join(
133
+ self.db_path,
134
+ file_type,
135
+ f"{self.id}.{file_type.lower()}{extension}"
136
+ )
137
+
138
+ os.makedirs(os.path.dirname(local_filename), exist_ok=True)
139
+
140
+ downloaded_size = 0
141
+ with open(local_filename, 'wb') as f:
142
+ def callback(data):
143
+ nonlocal downloaded_size
144
+ f.write(data)
145
+ downloaded_size += len(data)
146
+ if file_size:
147
+ progress = (downloaded_size / file_size) * 100
148
+ self.progress_updated.emit(self.id, progress, 100)
149
+
150
+ ftp.retrbinary(f'RETR {parsed_url.path[1:]}', callback)
151
+
152
+ ftp.quit()
153
+
154
+ if not os.path.exists(local_filename) or os.path.getsize(local_filename) == 0:
155
+ raise Exception(f"Downloaded file {local_filename} is empty or does not exist")
156
+
157
+ if is_gzipped:
158
+ self._decompress_file(local_filename)
159
+ else:
160
+ self.controller.model.add_downloaded_file(local_filename)
161
+
162
+ self.status_updated.emit(f"Download complete: {self.species_name}")
163
+
164
+ except Exception as e:
165
+ self.logger.error(f"Error in FTP download: {str(e)}")
166
+ raise
167
+
168
+ def _decompress_file(self, filename):
169
+ try:
170
+ with gzip.open(filename, 'rb') as f_in:
171
+ decompressed_filename = filename[:-3]
172
+ with open(decompressed_filename, 'wb') as f_out:
173
+ f_out.write(f_in.read())
174
+
175
+ if not os.path.exists(decompressed_filename) or os.path.getsize(decompressed_filename) == 0:
176
+ raise Exception(f"Decompressed file {decompressed_filename} is empty or does not exist")
177
+
178
+ os.remove(filename)
179
+ self.controller.model.add_downloaded_file(decompressed_filename)
180
+
181
+ except gzip.BadGzipFile:
182
+ self.logger.warning(f"File {filename} is not actually gzipped, renaming without .gz extension")
183
+ decompressed_filename = filename[:-3]
184
+ os.rename(filename, decompressed_filename)
185
+ self.controller.model.add_downloaded_file(decompressed_filename)
186
+
187
  def __init__(self, settings):
188
  self.settings = settings
189
  self.logger = settings.get_logger()
190
  self.df = pd.DataFrame()
191
  self.genbank_ftp_dict = {}
192
  self.refseq_ftp_dict = {}
193
+ self.files = []
194
+ self.current_database = None
195
+ self.current_search_params = {}
196
+ self.download_fna = False
197
+ self.download_gbff = False
198
+ Entrez.email = "your_email@example.com"
199
 
200
+ self.database_apis = {
201
+ "NCBI GenBank": self._search_ncbi,
202
+ "ENA (European Nucleotide Archive)": self._search_ena,
203
+ "UCSC Genome Browser": self._search_ucsc
204
+ }
205
 
206
  def search_ncbi(self, search_params):
207
+ """Search selected database with given parameters"""
208
+ database = search_params.get('database', "NCBI GenBank")
209
+ search_function = self.database_apis.get(database)
210
+
211
+ if search_function:
212
+ # Store current database and search parameters
213
+ self.current_database = database
214
+ self.current_search_params = search_params
215
+ return search_function(search_params)
216
+ else:
217
+ self.logger.error(f"Unsupported database: {database}")
218
+ raise ValueError(f"Unsupported database: {database}")
219
+
220
+ def _search_ncbi(self, search_params):
221
+ """Original NCBI search implementation"""
222
  try:
223
  retmax = int(search_params['max_results']) if search_params['max_results'] else 100
224
  term = f'"{search_params["organism"]}"[Organism]'
225
 
226
  if search_params['complete_genomes_only']:
227
  term += ' AND "Complete Genome"[Assembly Level]'
228
+ if search_params['strain'] and search_params['strain'].strip():
229
  term += f' AND "{search_params["strain"]}"[Infraspecific name]'
230
 
231
  self.logger.info(f"Searching NCBI with term: {term}")
 
261
  self.logger.error(f"Error in query_ncbi: {str(e)}", exc_info=True)
262
  raise
263
 
264
+ def _search_ena(self, search_params):
265
+ """Search ENA database"""
266
+ try:
267
+ organism = search_params['organism']
268
+ max_results = int(search_params['max_results'])
269
+
270
+ # ENA API endpoint for text search
271
+ base_url = "https://www.ebi.ac.uk/ena/portal/api/search"
272
+
273
+ # Construct query
274
+ query_parts = []
275
+ query_parts.append(f'scientific_name="{organism}"')
276
+
277
+ if search_params['strain']:
278
+ query_parts.append(f'strain="{search_params["strain"]}"')
279
+
280
+ if search_params['complete_genomes_only']:
281
+ query_parts.append('assembly_level="complete genome"')
282
+
283
+ query = " AND ".join(query_parts)
284
+
285
+ params = {
286
+ 'result': 'assembly',
287
+ 'query': query,
288
+ 'limit': max_results,
289
+ 'offset': 0,
290
+ 'format': 'json',
291
+ 'fields': 'accession,scientific_name,strain,assembly_level,assembly_name,study_accession'
292
+ }
293
+
294
+ self.logger.info(f"Searching ENA with parameters: {params}")
295
+ response = requests.get(base_url, params=params)
296
+
297
+ # Log the actual URL being called for debugging
298
+ self.logger.debug(f"ENA API URL: {response.url}")
299
+
300
+ # Check if the response is successful
301
+ if response.status_code != 200:
302
+ self.logger.error(f"ENA API error: {response.status_code} - {response.text}")
303
+ return pd.DataFrame()
304
+
305
+ # Parse JSON response
306
+ results = response.json()
307
+
308
+ if not results:
309
+ self.logger.info("No results returned from ENA")
310
+ return pd.DataFrame()
311
+
312
+ self.logger.info(f"Raw ENA response: {results[:2]}")
313
+
314
+ # Transform ENA results to match NCBI format
315
+ data = []
316
+ for result in results:
317
+ entry = {
318
+ 'ID': result.get('accession', 'N/A'),
319
+ 'Species Name': result.get('scientific_name', 'N/A'),
320
+ 'Strain': result.get('strain', 'N/A'),
321
+ 'Assembly Name': result.get('assembly_name', 'N/A'),
322
+ 'RefSeq assembly accession': 'Not Available',
323
+ 'GenBank assembly accession': result.get('accession', 'N/A'),
324
+ 'Assembly Status': result.get('assembly_level', 'N/A')
325
+ }
326
+ data.append(entry)
327
+
328
+ self.df = pd.DataFrame(data)
329
+ self.logger.info(f"Found {len(self.df)} results from ENA")
330
+ return self.df
331
+
332
+ except Exception as e:
333
+ self.logger.error(f"Error searching ENA: {str(e)}")
334
+ raise
335
+
336
+ def _search_ucsc(self, search_params):
337
+ """Search UCSC Genome Browser database"""
338
+ try:
339
+ organism = search_params['organism']
340
+ max_results = int(search_params['max_results'])
341
+
342
+ # Get list of genomes
343
+ base_url = "https://api.genome.ucsc.edu"
344
+ response = requests.get(f"{base_url}/list/ucscGenomes")
345
+ response.raise_for_status()
346
+
347
+ # Log raw response for debugging
348
+ self.logger.debug(f"UCSC API response: {response.text[:500]}")
349
+
350
+ data = response.json()
351
+ if 'ucscGenomes' not in data:
352
+ self.logger.warning("No ucscGenomes field in UCSC response")
353
+ return pd.DataFrame()
354
+
355
+ # Filter genomes by organism name
356
+ matching_genomes = []
357
+ for genome_id, genome in data['ucscGenomes'].items():
358
+ # Search in both organism and scientificName fields
359
+ if (organism.lower() in genome.get('organism', '').lower() or
360
+ organism.lower() in genome.get('scientificName', '').lower()):
361
+
362
+ # Get detailed track info
363
+ track_response = requests.get(f"{base_url}/list/tracks", params={'genome': genome_id})
364
+ if track_response.status_code == 200:
365
+ tracks = track_response.json()
366
+ genome['tracks'] = tracks
367
+ matching_genomes.append(genome)
368
+
369
+ if len(matching_genomes) >= max_results:
370
+ break
371
+
372
+ if not matching_genomes:
373
+ self.logger.info("No results returned from UCSC")
374
+ return pd.DataFrame()
375
+
376
+ # Transform UCSC results to match NCBI format
377
+ data = []
378
+ for genome in matching_genomes:
379
+ entry = {
380
+ 'ID': genome.get('genome', 'N/A'),
381
+ 'Species Name': genome.get('scientificName', genome.get('organism', 'N/A')),
382
+ 'Strain': genome.get('description', 'N/A'),
383
+ 'Assembly Name': genome.get('sourceName', 'N/A'),
384
+ 'RefSeq assembly accession': 'Not Available', # UCSC doesn't provide RefSeq accessions
385
+ 'GenBank assembly accession': genome.get('sourceName', 'Not Available'),
386
+ 'Assembly Status': 'Complete' if genome.get('active') else 'N/A'
387
+ }
388
+ data.append(entry)
389
+
390
+ self.df = pd.DataFrame(data)
391
+ self.logger.info(f"Found {len(self.df)} results from UCSC")
392
+ return self.df
393
+
394
+ except Exception as e:
395
+ self.logger.error(f"Error searching UCSC: {str(e)}")
396
+ raise
397
+
398
  def _process_ncbi_data(self, summary_results):
399
  data = []
400
  for assembly in summary_results['DocumentSummarySet']['DocumentSummary']:
401
  gb_ftp = assembly.get('FtpPath_GenBank', '')
402
  rs_ftp = assembly.get('FtpPath_RefSeq', '')
403
 
404
+ # Get assembly accession and uid
405
+ assembly_accession = assembly.get('AssemblyAccession', 'N/A')
406
+ uid = assembly.attributes['uid']
407
+
408
+ # Safely get the strain information
409
+ biosource = assembly.get('Biosource', {})
410
+ infraspecies_list = biosource.get('InfraspeciesList', [])
411
+ strain = (infraspecies_list[0].get('Sub_value', 'N/A')
412
+ if infraspecies_list
413
+ else 'N/A')
414
+
415
  entry = {
416
+ 'ID': uid,
417
  'Species Name': assembly.get('SpeciesName', 'N/A'),
418
+ 'Strain': strain,
419
  'Assembly Name': assembly.get('AssemblyName', 'N/A'),
420
+ 'RefSeq assembly accession': assembly_accession if rs_ftp else 'Not Available',
421
+ 'GenBank assembly accession': assembly_accession if gb_ftp else 'Not Available',
422
  'Assembly Status': assembly.get('AssemblyStatus', 'N/A')
423
  }
424
 
425
+ # Store FTP paths using both UID and assembly accession as keys
426
  if gb_ftp:
427
+ self.genbank_ftp_dict[uid] = gb_ftp
428
+ self.genbank_ftp_dict[assembly_accession] = gb_ftp
429
+
430
  if rs_ftp:
431
+ self.refseq_ftp_dict[uid] = rs_ftp
432
+ self.refseq_ftp_dict[assembly_accession] = rs_ftp
433
+
434
  data.append(entry)
435
 
436
  self.df = pd.DataFrame(data)
 
440
  self.logger.warning("No data found in NCBI response")
441
  else:
442
  self.logger.info(f"Processed {len(self.df)} entries from NCBI response")
443
+ self.logger.debug(f"GenBank FTP dict has {len(self.genbank_ftp_dict)} entries")
444
+ self.logger.debug(f"RefSeq FTP dict has {len(self.refseq_ftp_dict)} entries")
 
 
445
 
446
  def _get_element_text(self, element, tag):
447
  el = element.find(tag)
448
  return el.text if el is not None else 'N/A'
449
 
450
  def get_download_url(self, id, use_genbank):
451
+ """Get download URL based on selected database and parameters"""
452
+ try:
453
+ database = self.current_database
454
+
455
+ # Set download flags based on search parameters
456
+ self.download_fna = self.current_search_params.get('fna', False)
457
+ self.download_gbff = self.current_search_params.get('gbff', False)
458
+
459
+ if database == "NCBI GenBank":
460
+ return self._get_ncbi_download_url(id, use_genbank)
461
+ elif database == "ENA (European Nucleotide Archive)":
462
+ return self._get_ena_download_url(id)
463
+ elif database == "UCSC Genome Browser":
464
+ self.logger.warning("Downloads not supported for UCSC Genome Browser")
465
+ return None
466
+
467
+ return None
468
+ except Exception as e:
469
+ self.logger.error(f"Error getting download URL: {str(e)}")
470
+ return None
471
+
472
+ def _get_ena_download_url(self, id):
473
+ """Get download URL for ENA database"""
474
+ try:
475
+ urls = []
476
+
477
+ # Try to get FASTA (FNA) file
478
+ fasta_url = f"https://www.ebi.ac.uk/ena/browser/api/fasta/{id}?download=true&gzip=true"
479
+ test_response = requests.head(fasta_url)
480
+ if test_response.status_code == 200:
481
+ self.logger.info(f"Found direct FASTA download URL for {id}")
482
+ urls.append(fasta_url)
483
+
484
+ # Try to get EMBL (GBFF) file
485
+ embl_url = f"https://www.ebi.ac.uk/ena/browser/api/embl/{id}?download=true&gzip=true"
486
+ test_response = requests.head(embl_url)
487
+ if test_response.status_code == 200:
488
+ self.logger.info(f"Found direct EMBL download URL for {id}")
489
+ urls.append(embl_url)
490
+
491
+ # If no direct downloads found, try alternative sources
492
+ if not urls:
493
+ # Try WGS format if it's a WGS accession
494
+ if len(id) >= 6 and id.startswith('GCA_'):
495
+ wgs_id = id.split('_')[1] # Get the numeric part
496
+ wgs_fasta_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/wgs/{wgs_id[:6].lower()}/{wgs_id}.fasta.gz"
497
+ wgs_embl_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/wgs/{wgs_id[:6].lower()}/{wgs_id}.embl.gz"
498
+
499
+ test_response = requests.head(wgs_fasta_url)
500
+ if test_response.status_code == 200:
501
+ self.logger.info(f"Found WGS FASTA download URL for {id}")
502
+ urls.append(wgs_fasta_url)
503
+
504
+ test_response = requests.head(wgs_embl_url)
505
+ if test_response.status_code == 200:
506
+ self.logger.info(f"Found WGS EMBL download URL for {id}")
507
+ urls.append(wgs_embl_url)
508
+
509
+ # Try assembly FTP path
510
+ assembly_fasta_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/assembly/{id}/{id}.fasta.gz"
511
+ assembly_embl_url = f"https://ftp.ebi.ac.uk/pub/databases/ena/assembly/{id}/{id}.embl.gz"
512
+
513
+ test_response = requests.head(assembly_fasta_url)
514
+ if test_response.status_code == 200:
515
+ self.logger.info(f"Found assembly FASTA URL for {id}")
516
+ urls.append(assembly_fasta_url)
517
+
518
+ test_response = requests.head(assembly_embl_url)
519
+ if test_response.status_code == 200:
520
+ self.logger.info(f"Found assembly EMBL URL for {id}")
521
+ urls.append(assembly_embl_url)
522
+
523
+ if urls:
524
+ self.logger.info(f"Found {len(urls)} download URLs for {id}: {urls}")
525
+ return urls
526
+
527
+ self.logger.warning(f"No suitable download URLs found for accession {id}")
528
+ return None
529
+
530
+ except Exception as e:
531
+ self.logger.error(f"Error getting ENA download URLs for {id}: {str(e)}")
532
+ return None
533
+
534
+ def _get_ncbi_download_url(self, id, use_genbank):
535
+ try:
536
+ # First try with the ID directly
537
+ base_url = None
538
+ if use_genbank:
539
+ base_url = self.genbank_ftp_dict.get(id)
540
+ else:
541
+ base_url = self.refseq_ftp_dict.get(id)
542
+
543
+ # If not found, try to find the corresponding assembly accession in our DataFrame
544
+ if not base_url and id in self.df.index:
545
+ row = self.df.loc[id]
546
+ accession = row['GenBank assembly accession'] if use_genbank else row['RefSeq assembly accession']
547
+ if accession != 'Not Available':
548
+ if use_genbank:
549
+ base_url = self.genbank_ftp_dict.get(accession)
550
+ else:
551
+ base_url = self.refseq_ftp_dict.get(accession)
552
+
553
+ if not base_url:
554
+ self.logger.warning(f"No FTP path found for ID {id}")
555
+ return None
556
+
557
+ # Remove trailing slash if present
558
+ base_url = base_url.rstrip('/')
559
+
560
+ # Get the assembly accession from the base URL
561
+ assembly_dir = os.path.basename(base_url)
562
+
563
+ # Return both FNA and GBFF URLs if they exist
564
+ urls = []
565
+
566
+ # Check FNA file
567
+ fna_url = f"{base_url}/{assembly_dir}_genomic.fna.gz"
568
+ gbff_url = f"{base_url}/{assembly_dir}_genomic.gbff.gz"
569
+
570
+ # Test if URLs exist
571
+ try:
572
+ parsed_url = urlparse(fna_url)
573
+ ftp = FTP(parsed_url.netloc)
574
+ ftp.login()
575
+
576
+ # Check FNA file
577
+ try:
578
+ ftp.size(parsed_url.path[1:])
579
+ urls.append(fna_url)
580
+ except Exception:
581
+ # Try alternative FNA name format
582
+ alt_fna_url = f"{base_url}/{assembly_dir}.fna.gz"
583
+ try:
584
+ ftp.size(urlparse(alt_fna_url).path[1:])
585
+ urls.append(alt_fna_url)
586
+ except:
587
+ pass
588
+
589
+ # Check GBFF file
590
+ try:
591
+ ftp.size(urlparse(gbff_url).path[1:])
592
+ urls.append(gbff_url)
593
+ except Exception:
594
+ # Try alternative GBFF name format
595
+ alt_gbff_url = f"{base_url}/{assembly_dir}.gbff.gz"
596
+ try:
597
+ ftp.size(urlparse(alt_gbff_url).path[1:])
598
+ urls.append(alt_gbff_url)
599
+ except:
600
+ pass
601
+
602
+ ftp.quit()
603
+
604
+ return urls if urls else None
605
+
606
+ except Exception as e:
607
+ self.logger.error(f"Error checking FTP URLs for {id}: {str(e)}")
608
+ return None
609
+
610
+ except Exception as e:
611
+ self.logger.error(f"Error getting NCBI download URL for {id}: {str(e)}")
612
+ return None
613
 
614
  def decompress_file(self, filename):
615
  try:
 
628
  raise
629
 
630
  def get_output_path(self, file_type):
631
+ db_path = self.settings.get_db_path()
632
+ self.logger.debug(f"Using database path for downloads: {db_path}")
633
+ return os.path.join(db_path, file_type)
 
634
 
635
  def rename_file(self, old_name, new_name, file_type):
636
  old_path = os.path.join(self.get_output_path(file_type), old_name)
 
728
  if regex.match(text).hasMatch():
729
  return False
730
  return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/models/NewEndonucleaseModel.py CHANGED
@@ -37,19 +37,30 @@ class NewEndonucleaseModel(QObject):
37
  return [], [] # Return empty lists if there's an error
38
 
39
  def create_new_endonuclease(self, new_endonuclease_str):
 
40
  try:
41
- new_file_path = os.path.join(self.app_dir_path, "new_file")
42
- with open(self.casper_info_path, 'r') as f, open(new_file_path, 'w+') as f1:
 
 
43
  for line in f:
44
- f1.write(line)
45
- if 'ENDONUCLEASES' in line:
46
- f1.write(new_endonuclease_str + '\n')
47
- os.remove(self.casper_info_path)
48
- os.rename(new_file_path, self.casper_info_path)
 
 
 
 
 
 
 
49
  self.global_settings.config_manager.load_endonucleases_data()
50
  self.endonuclease_updated.emit()
 
51
  except Exception as e:
52
- show_error(self.global_settings, "Error in create_new_endonuclease() in New Endonuclease Model.", str(e))
53
 
54
  def is_duplicate_abbreviation(self, abbr):
55
  try:
@@ -66,76 +77,90 @@ class NewEndonucleaseModel(QObject):
66
  return False
67
 
68
  def create_endonuclease_string(self, form_data):
69
- pam = form_data['endonuclease_pam_sequence']
70
- if ',' in pam:
71
- pam = ','.join([x.strip() for x in pam.split(',')])
72
-
73
  argument_list = [
74
  form_data['endonuclease_abbreviation'],
75
- pam,
76
  form_data['endonuclease_five_prime_length'],
77
  form_data['endonuclease_seed_length'],
78
  form_data['endonuclease_three_prime_length'],
79
- form_data['endonuclease_direction'],
80
  form_data['endonuclease_organism'],
81
  form_data['endonuclease_CRISPR_type'],
82
  form_data['endonuclease_on_target_scoring'],
83
  form_data['endonuclease_off_target_scoring']
84
  ]
85
-
86
  return ";".join(str(arg) for arg in argument_list)
87
 
88
  def update_endonuclease(self, selected, form_data):
 
89
  try:
90
  new_endonuclease_str = self.create_endonuclease_string(form_data)
 
 
 
 
91
  with open(self.casper_info_path, 'r') as f:
92
- lines = f.readlines()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- updated = False
95
- selected_abbr = selected.split(' - ')[0]
96
- for i, line in enumerate(lines):
97
- if line.startswith('ENDONUCLEASES'):
98
- continue
99
- fields = line.strip().split(';')
100
- if len(fields) >= 2 and fields[1] == selected_abbr:
101
- lines[i] = new_endonuclease_str + '\n'
102
- updated = True
103
- break
104
 
105
- if updated:
106
- with open(self.casper_info_path, 'w') as f:
107
- f.writelines(lines)
108
- self.global_settings.config_manager.load_endonucleases_data()
109
- self.endonuclease_updated.emit()
110
- else:
111
- raise ValueError(f"Endonuclease '{selected}' not found in CASPERinfo file")
112
  except Exception as e:
113
  show_error(self.global_settings, "Error updating endonuclease", str(e))
114
 
115
  def delete_endonuclease(self, selected):
 
116
  try:
117
- with open(self.casper_info_path, 'r') as f:
118
- lines = f.readlines()
119
-
120
- deleted = False
121
  new_lines = []
 
122
  selected_abbr = selected.split(' - ')[0]
123
- for line in lines:
124
- if line.startswith('ENDONUCLEASES'):
 
 
 
 
 
 
 
 
 
 
 
 
125
  new_lines.append(line)
126
- continue
127
- fields = line.strip().split(';')
128
- if len(fields) >= 2 and fields[1] == selected_abbr:
129
- deleted = True
130
- continue # Skip this line to delete it
131
- new_lines.append(line) # Keep all other lines
132
 
133
- if deleted:
134
- with open(self.casper_info_path, 'w') as f:
135
- f.writelines(new_lines)
136
- self.global_settings.config_manager.load_endonucleases_data()
137
- self.endonuclease_updated.emit()
138
- else:
139
- raise ValueError(f"Endonuclease '{selected}' not found in CASPERinfo file")
 
 
140
  except Exception as e:
141
  show_error(self.global_settings, "Error deleting endonuclease", str(e))
 
37
  return [], [] # Return empty lists if there's an error
38
 
39
  def create_new_endonuclease(self, new_endonuclease_str):
40
+ """Add a new endonuclease to CASPERinfo file"""
41
  try:
42
+ found_section = False
43
+ new_lines = []
44
+
45
+ with open(self.casper_info_path, 'r') as f:
46
  for line in f:
47
+ new_lines.append(line)
48
+ if line.strip() == 'ENDONUCLEASES':
49
+ found_section = True
50
+ new_lines.append(new_endonuclease_str + '\n')
51
+
52
+ if not found_section:
53
+ new_lines.append('ENDONUCLEASES\n')
54
+ new_lines.append(new_endonuclease_str + '\n')
55
+
56
+ with open(self.casper_info_path, 'w') as f:
57
+ f.writelines(new_lines)
58
+
59
  self.global_settings.config_manager.load_endonucleases_data()
60
  self.endonuclease_updated.emit()
61
+
62
  except Exception as e:
63
+ show_error(self.global_settings, "Error in create_new_endonuclease()", str(e))
64
 
65
  def is_duplicate_abbreviation(self, abbr):
66
  try:
 
77
  return False
78
 
79
  def create_endonuclease_string(self, form_data):
80
+ """Create a properly formatted endonuclease string for CASPERinfo"""
81
+ # Format: abbr;PAM;5_prime_length;seed_length;3_prime_length;direction;organism;CRISPR_type;on_target;off_target
 
 
82
  argument_list = [
83
  form_data['endonuclease_abbreviation'],
84
+ form_data['endonuclease_pam_sequence'],
85
  form_data['endonuclease_five_prime_length'],
86
  form_data['endonuclease_seed_length'],
87
  form_data['endonuclease_three_prime_length'],
88
+ '3' if form_data['endonuclease_direction'] == '3' else '5',
89
  form_data['endonuclease_organism'],
90
  form_data['endonuclease_CRISPR_type'],
91
  form_data['endonuclease_on_target_scoring'],
92
  form_data['endonuclease_off_target_scoring']
93
  ]
 
94
  return ";".join(str(arg) for arg in argument_list)
95
 
96
  def update_endonuclease(self, selected, form_data):
97
+ """Update an existing endonuclease in CASPERinfo file"""
98
  try:
99
  new_endonuclease_str = self.create_endonuclease_string(form_data)
100
+ new_lines = []
101
+ updated = False
102
+ selected_abbr = selected.split(' - ')[0] # Get abbreviation part
103
+
104
  with open(self.casper_info_path, 'r') as f:
105
+ in_endo_section = False
106
+ for line in f:
107
+ if line.strip() == 'ENDONUCLEASES':
108
+ in_endo_section = True
109
+ new_lines.append(line)
110
+ continue
111
+
112
+ if in_endo_section and line.strip():
113
+ fields = line.strip().split(';')
114
+ if fields[0] == selected_abbr:
115
+ new_lines.append(new_endonuclease_str + '\n')
116
+ updated = True
117
+ else:
118
+ new_lines.append(line)
119
+ else:
120
+ new_lines.append(line)
121
 
122
+ if not updated:
123
+ raise ValueError(f"Endonuclease '{selected}' not found")
124
+
125
+ with open(self.casper_info_path, 'w') as f:
126
+ f.writelines(new_lines)
127
+
128
+ self.global_settings.config_manager.load_endonucleases_data()
129
+ self.endonuclease_updated.emit()
 
 
130
 
 
 
 
 
 
 
 
131
  except Exception as e:
132
  show_error(self.global_settings, "Error updating endonuclease", str(e))
133
 
134
  def delete_endonuclease(self, selected):
135
+ """Delete an endonuclease from CASPERinfo file"""
136
  try:
 
 
 
 
137
  new_lines = []
138
+ deleted = False
139
  selected_abbr = selected.split(' - ')[0]
140
+
141
+ with open(self.casper_info_path, 'r') as f:
142
+ in_endo_section = False
143
+ for line in f:
144
+ if line.strip() == 'ENDONUCLEASES':
145
+ in_endo_section = True
146
+ new_lines.append(line)
147
+ continue
148
+
149
+ if in_endo_section and line.strip():
150
+ fields = line.strip().split(';')
151
+ if fields[0] == selected_abbr:
152
+ deleted = True
153
+ continue
154
  new_lines.append(line)
 
 
 
 
 
 
155
 
156
+ if not deleted:
157
+ raise ValueError(f"Endonuclease '{selected}' not found")
158
+
159
+ with open(self.casper_info_path, 'w') as f:
160
+ f.writelines(new_lines)
161
+
162
+ self.global_settings.config_manager.load_endonucleases_data()
163
+ self.endonuclease_updated.emit()
164
+
165
  except Exception as e:
166
  show_error(self.global_settings, "Error deleting endonuclease", str(e))
src/models/NewGenomeWindowModel.py CHANGED
@@ -66,12 +66,11 @@ class NewGenomeWindowModel(QObject):
66
  def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
67
  db_path = self.settings.get_db_path()
68
 
69
- # Preserve trailing slash if present
70
- if db_path.endswith(os.path.sep):
71
- db_path = db_path.rstrip(os.path.sep) + os.path.sep
72
- # Add trailing slash for Darwin (macOS) machines
73
- elif platform.system() == 'Darwin':
74
- db_path = db_path.rstrip('/') + '/'
75
 
76
  print(f"The endonuclease data is {endonuclease_data}")
77
 
@@ -85,13 +84,17 @@ class NewGenomeWindowModel(QObject):
85
  endonuclease_data['endonuclease_seed_length'],
86
  endonuclease_data['endonuclease_three_prime_length'],
87
  organism_code,
88
- f'{db_path}',
89
- f'{self.settings.get_casper_info_path()}',
90
- f'{file_path}',
91
  f'{organism_name} {strain}',
92
  'notes',
93
  f'DATA:{endonuclease_data["endonuclease_on_target_scoring"]}'
94
  ]
 
 
 
 
95
  return arguments
96
 
97
  def get_arguments_command_for_job(self, job_index):
@@ -103,7 +106,7 @@ class NewGenomeWindowModel(QObject):
103
  if platform.system() == 'Windows':
104
  program = f'"{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Win.exe")}" '
105
  elif platform.system() == 'Linux':
106
- program = f'"{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Lin")}" '
107
  else:
108
  program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Mac")}'
109
  return program
@@ -145,3 +148,10 @@ class NewGenomeWindowModel(QObject):
145
  if not endonuclease:
146
  return None
147
  return self.endonucleases.get(endonuclease, None)
 
 
 
 
 
 
 
 
66
  def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
67
  db_path = self.settings.get_db_path()
68
 
69
+ # Ensure db_path ends with a forward slash
70
+ if not db_path.endswith('/'):
71
+ db_path = f"{db_path}/"
72
+
73
+ self.logger.debug(f"Using database path: {db_path}") # Add logging
 
74
 
75
  print(f"The endonuclease data is {endonuclease_data}")
76
 
 
84
  endonuclease_data['endonuclease_seed_length'],
85
  endonuclease_data['endonuclease_three_prime_length'],
86
  organism_code,
87
+ db_path, # This will now always end with a forward slash
88
+ self.settings.get_casper_info_path(),
89
+ file_path,
90
  f'{organism_name} {strain}',
91
  'notes',
92
  f'DATA:{endonuclease_data["endonuclease_on_target_scoring"]}'
93
  ]
94
+
95
+ # Add logging of the full command
96
+ self.logger.debug(f"Generated command arguments: {arguments}")
97
+
98
  return arguments
99
 
100
  def get_arguments_command_for_job(self, job_index):
 
106
  if platform.system() == 'Windows':
107
  program = f'"{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Win.exe")}" '
108
  elif platform.system() == 'Linux':
109
+ program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Lin")}'
110
  else:
111
  program = f'{os.path.join(self.settings.get_SeqFinder_dir_path(), "Casper_Seq_Finder_Mac")}'
112
  return program
 
148
  if not endonuclease:
149
  return None
150
  return self.endonucleases.get(endonuclease, None)
151
+
152
+ def get_job_name(self, job_index):
153
+ """Get the name of the job at the given index"""
154
+ if 0 <= job_index < len(self.jobs):
155
+ job_entry = self.jobs[job_index]
156
+ return next(iter(job_entry)) # Returns the first (and only) key
157
+ return None
src/models/OffTarget/local_output.txt CHANGED
@@ -1,8 +1,4 @@
1
  DETAILED OUTPUT
2
- CACTTATGACCGGGCAACTT:0.000000
3
- ACACTTATGACCGGGCAACT:0.080598
4
- 0.080598,1,-100695,ACAATTACGCCCGGGCAACC
5
- TCAAAATAGCCCAAGTTGCC:0.000000
6
  ATTTTGCTACACTTATGACC:0.000000
7
  AATTTTGCTACACTTATGAC:0.000000
8
  GGGAATACTCCCTTTTATTG:0.000000
 
1
  DETAILED OUTPUT
 
 
 
 
2
  ATTTTGCTACACTTATGACC:0.000000
3
  AATTTTGCTACACTTATGAC:0.000000
4
  GGGAATACTCCCTTTTATTG:0.000000
src/models/StartupWindowModel.py CHANGED
@@ -15,9 +15,13 @@ class StartupWindowModel(QObject):
15
  return self.settings.get_db_path()
16
 
17
  def save_db_path(self, directory_path):
 
 
 
18
  success, message = self.settings.save_db_path(directory_path)
19
- # Note: The actual db_state_updated signal will be emitted by the DatabaseManager
20
 
21
  def on_db_state_updated(self, is_valid, message, cspr_files):
 
22
  self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}, cspr_files_count={len(cspr_files)}")
23
  self.db_state_updated.emit(is_valid, message, cspr_files)
 
15
  return self.settings.get_db_path()
16
 
17
  def save_db_path(self, directory_path):
18
+ """Save the database path and trigger validation"""
19
+ self.logger.debug(f"Saving database path: {directory_path}")
20
+ # The db_manager will emit its own signals that we're now listening to
21
  success, message = self.settings.save_db_path(directory_path)
22
+ return success, message
23
 
24
  def on_db_state_updated(self, is_valid, message, cspr_files):
25
+ """Handle database state updates"""
26
  self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}, cspr_files_count={len(cspr_files)}")
27
  self.db_state_updated.emit(is_valid, message, cspr_files)
src/models/ViewTargetsModel.py CHANGED
@@ -3,13 +3,8 @@ from models.HomeWindowModel import HomeWindowModel
3
  from models.AnnotationParser import AnnotationParser
4
  import os
5
  from Bio import SeqIO
6
- from Bio.Seq import Seq
7
- from functools import lru_cache
8
- import threading
9
  from collections import defaultdict
10
- import re
11
  import traceback
12
- import logging
13
 
14
  class ViewTargetsModel(HomeWindowModel):
15
  def __init__(self, global_settings):
@@ -110,28 +105,15 @@ class ViewTargetsModel(HomeWindowModel):
110
 
111
  # Initialize guides and genes
112
  self.guides = []
113
- self.available_genes = set()
114
 
115
  # Use a set to track unique guide positions
116
  seen_guides = set()
117
 
118
- # Create chromosome mapping by counting carets
119
- chrom_mapping = {}
120
- chrom_count = 0
121
- annotation_file = self.global_settings.get_current_annotation_file()
122
- annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
123
-
124
- for record in SeqIO.parse(annotation_path, "genbank"):
125
- chrom_count += 1
126
- chrom_mapping[record.id] = str(chrom_count)
127
-
128
  batch_guides = defaultdict(list)
129
  for target in selected_targets:
130
- # Get chromosome number from mapping if full_chromosome is available
131
- if 'full_chromosome' in target:
132
- chrom = chrom_mapping.get(target['full_chromosome'], target['chromosome'])
133
- else:
134
- chrom = target['chromosome']
135
 
136
  start, end = map(int, target['location'].split('-'))
137
 
@@ -147,11 +129,13 @@ class ViewTargetsModel(HomeWindowModel):
147
  'start': start,
148
  'end': end
149
  })
150
- self.available_genes.add((target['feature_id'], target['feature_name']))
 
151
 
152
  # Process guides by chromosome
153
  unique_guides = {} # Use dict to track unique guides by sequence
154
  for chrom, guides in batch_guides.items():
 
155
  results = self.cspr_parser.read_targets_batch(chrom, guides, endonuclease)
156
 
157
  # Add feature_id to each result and deduplicate
@@ -172,6 +156,7 @@ class ViewTargetsModel(HomeWindowModel):
172
  self.guides = list(unique_guides.values())
173
 
174
  self.logger.debug(f"Found {len(self.guides)} unique guides")
 
175
 
176
  except Exception as e:
177
  self.logger.error(f"Error in load_guides: {str(e)}")
@@ -252,79 +237,77 @@ class ViewTargetsModel(HomeWindowModel):
252
  self.logger.error(f"Error getting available genes: {str(e)}")
253
  return []
254
 
255
- def _process_guide(self, guide):
256
- """Process a single guide - moved to separate method for parallel processing"""
257
- try:
258
- # Your existing guide processing logic here
259
- # Make sure to handle any shared resources thread-safely
260
- pass
261
- except Exception as e:
262
- logging.error(f"Error processing guide: {e}")
263
- return None
264
-
265
  def get_gene_sequence(self, identifier):
266
  """Get gene sequence with optimized caching and minimal I/O"""
267
  try:
268
- print(f"Getting gene sequence for identifier: {identifier}")
269
- # Check sequence cache first
270
- cache_key = f"{identifier}_sequence"
271
- if cache_key in self._sequence_cache:
272
- self.logger.debug(f"Cache hit for sequence: {identifier}")
273
- return self._sequence_cache[cache_key]
274
-
275
- # Check if this is a position-based search
276
- if "chrom" in identifier and "start:" in identifier:
277
- try:
278
- # Parse position from the text (format: "chrom X, start: Y, end: Z")
279
- parts = identifier.split(',')
280
- chrom = int(parts[0].split('chrom')[1].strip())
281
- start = int(parts[1].split('start:')[1].strip())
282
- end = int(parts[2].split('end:')[1].strip())
 
 
 
 
283
 
284
- # Get sequence directly using _get_sequence_for_position
285
- sequence = self._get_sequence_for_position(chrom, start, end)
286
- if sequence:
287
- result = {
288
- 'sequence': sequence,
289
- 'start': start,
290
- 'end': end,
291
- 'chrom_length': len(sequence)
292
- }
293
- self._sequence_cache[cache_key] = result
294
- self.logger.debug(f"Retrieved and cached position sequence ({len(sequence)} bp)")
295
- return result
 
 
 
 
 
 
 
 
 
296
 
297
- self.logger.warning(f"No sequence found for position {chrom}:{start}-{end}")
298
- return None
 
 
299
 
300
- except Exception as e:
301
- self.logger.error(f"Error parsing position or getting sequence: {str(e)}")
302
- return None
303
- else:
304
- # Regular gene-based search
305
- self.logger.debug(f"Getting gene data for locus tag: {identifier}")
306
- gene_data = self.get_gene_data(identifier)
307
- if not gene_data or 'info' not in gene_data:
308
- self.logger.warning(f"No gene data found for locus tag: {identifier}")
309
- return None
310
-
311
- # Parse location string (format: "start:end(strand)")
312
- location = gene_data['info']['location']
313
- if ':' not in location:
314
- self.logger.warning(f"Invalid location format: {location}")
315
- return None
316
-
317
- # Extract start and end positions
318
- start = int(location.split(':')[0])
319
- end = int(location.split(':')[1].split('(')[0])
320
-
321
- # Get sequence from gene_data directly if available
322
- if 'sequence' in gene_data:
323
- sequence = gene_data['sequence']
324
- self.logger.debug(f"Got sequence of length: {len(sequence)}")
325
 
326
- # Format sequence with padding in lowercase
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  padding = 30
 
 
328
  padded_start = max(0, start - padding)
329
  padded_end = min(len(sequence), end + padding)
330
 
@@ -335,24 +318,23 @@ class ViewTargetsModel(HomeWindowModel):
335
 
336
  # Combine parts
337
  formatted_sequence = five_prime_pad + main_sequence + three_prime_pad
338
-
339
- # Cache the result
340
- result = {
341
- 'sequence': formatted_sequence,
342
- 'chrom_length': len(sequence),
343
- 'start': start,
344
- 'end': end,
345
- 'padded_start': padded_start,
346
- 'padded_end': padded_end
347
- }
348
- self._sequence_cache[cache_key] = result
349
-
350
- self.logger.debug(f"Retrieved and cached sequence for locus tag {identifier} ({len(formatted_sequence)} bp)")
351
- return result
352
-
353
- self.logger.warning(f"No sequence data found in gene_data for {identifier}")
354
- return None
355
 
 
 
 
 
 
 
356
  except Exception as e:
357
  self.logger.error(f"Error getting gene sequence: {str(e)}")
358
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
@@ -367,28 +349,9 @@ class ViewTargetsModel(HomeWindowModel):
367
  annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
368
  self.annotation_parser.set_annotation_file(annotation_path)
369
 
370
- # Get the full chromosome ID by counting carets in annotation file
371
- full_chrom = None
372
- chrom_count = 0
373
-
374
- try:
375
- for record in SeqIO.parse(self.annotation_path, "genbank"):
376
- chrom_count += 1
377
- if chrom_count == int(chrom): # Match based on position rather than ID number
378
- full_chrom = record.id
379
- self.logger.debug(f"Found chromosome {chrom} as {full_chrom}")
380
- break
381
- except Exception as e:
382
- self.logger.error(f"Error finding chromosome by position: {str(e)}")
383
- return None
384
-
385
- if not full_chrom:
386
- self.logger.warning(f"Could not find chromosome at position {chrom}")
387
- return None
388
-
389
  feature_info = {
390
- 'chromosome': full_chrom,
391
- 'start': start-1,
392
  'end': end
393
  }
394
 
@@ -449,12 +412,13 @@ class ViewTargetsModel(HomeWindowModel):
449
  if not gene_data or 'info' not in gene_data:
450
  self.logger.warning(f"No gene data found for identifier: {identifier}")
451
  return None
452
-
453
- # Get chromosome from gene data
454
- chrom = gene_data['info']['chromosome'].split('.')[-1] # Extract chromosome number
 
455
 
456
  # Use _get_sequence_for_position to get sequence with padding
457
- sequence = self._get_sequence_for_position(int(chrom), start, end)
458
  if sequence:
459
  result = {
460
  'sequence': sequence,
@@ -469,4 +433,89 @@ class ViewTargetsModel(HomeWindowModel):
469
  except Exception as e:
470
  self.logger.error(f"Error getting gene sequence for range: {str(e)}")
471
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
472
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from models.AnnotationParser import AnnotationParser
4
  import os
5
  from Bio import SeqIO
 
 
 
6
  from collections import defaultdict
 
7
  import traceback
 
8
 
9
  class ViewTargetsModel(HomeWindowModel):
10
  def __init__(self, global_settings):
 
105
 
106
  # Initialize guides and genes
107
  self.guides = []
108
+ self.available_genes = set() # Clear existing genes
109
 
110
  # Use a set to track unique guide positions
111
  seen_guides = set()
112
 
 
 
 
 
 
 
 
 
 
 
113
  batch_guides = defaultdict(list)
114
  for target in selected_targets:
115
+ # Use full_chromosome directly if available, otherwise use chromosome
116
+ chrom = target.get('full_chromosome', target['chromosome'])
 
 
 
117
 
118
  start, end = map(int, target['location'].split('-'))
119
 
 
129
  'start': start,
130
  'end': end
131
  })
132
+ # Add only the feature_id from the original target
133
+ self.available_genes.add(target['feature_id'])
134
 
135
  # Process guides by chromosome
136
  unique_guides = {} # Use dict to track unique guides by sequence
137
  for chrom, guides in batch_guides.items():
138
+ # Use full chromosome ID for CSPR lookup
139
  results = self.cspr_parser.read_targets_batch(chrom, guides, endonuclease)
140
 
141
  # Add feature_id to each result and deduplicate
 
156
  self.guides = list(unique_guides.values())
157
 
158
  self.logger.debug(f"Found {len(self.guides)} unique guides")
159
+ self.logger.debug(f"Available genes: {self.available_genes}")
160
 
161
  except Exception as e:
162
  self.logger.error(f"Error in load_guides: {str(e)}")
 
237
  self.logger.error(f"Error getting available genes: {str(e)}")
238
  return []
239
 
 
 
 
 
 
 
 
 
 
 
240
  def get_gene_sequence(self, identifier):
241
  """Get gene sequence with optimized caching and minimal I/O"""
242
  try:
243
+ self.logger.debug(f"Getting gene sequence for identifier: {identifier}")
244
+ self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
245
+
246
+ # Regular gene-based search
247
+ self.logger.debug(f"Getting gene data for locus tag: {identifier}")
248
+ gene_data = self.get_gene_data(identifier)
249
+ if not gene_data or 'info' not in gene_data:
250
+ self.logger.warning(f"No gene data found for locus tag: {identifier}")
251
+ return None
252
+
253
+ # Check if we're in exons-only mode
254
+ if getattr(self, '_view_exons_only', False):
255
+ print(f"gene_data: {gene_data}")
256
+ full_location = gene_data['info'].get('full_location', '')
257
+ print(f"Full location: {full_location}")
258
+ if full_location and ',' in full_location: # Multiple exons
259
+ self.logger.debug(f"Processing exons from full location: {full_location}")
260
+ exon_sequences = []
261
+ full_sequence = gene_data['sequence'] # Use sequence from gene_data
262
 
263
+ # Calculate padding offset
264
+ padding = 30
265
+ gene_start = gene_data['info']['start']
266
+ padded_start = max(0, gene_start - padding)
267
+ padding_offset = gene_start - padded_start
268
+
269
+ print(f"gene_start: {gene_start}, padded_start: {padded_start}, padding_offset: {padding_offset}")
270
+
271
+ # Process each exon location
272
+ for exon in full_location.split(','):
273
+ # Extract coordinates and strand
274
+ coords = exon.split('(')[0] # Get part before strand
275
+ strand = exon.split('(')[1][0] # Get + or - from (+ or (-
276
+ start, end = map(int, coords.split('..'))
277
+
278
+ print(f"coords: {coords}, strand: {strand}, start: {start}, end: {end}")
279
+
280
+ # Adjust coordinates relative to gene start and account for padding
281
+ relative_start = start - gene_start + padding_offset
282
+ relative_end = end - gene_start + padding_offset
283
+ print(f"relative_start: {relative_start}, relative_end: {relative_end}")
284
 
285
+ # Get exon sequence from the padded sequence
286
+ exon_seq = full_sequence[relative_start:relative_end]
287
+
288
+ exon_sequences.append(exon_seq)
289
 
290
+ # Join exon sequences
291
+ sequence = ''.join(exon_sequences)
292
+ self.logger.debug(f"Created concatenated exon sequence of length: {len(sequence)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
+ return {
295
+ 'sequence': sequence,
296
+ 'info': gene_data['info'],
297
+ 'start': gene_data['info']['start'],
298
+ 'end': gene_data['info']['end']
299
+ }
300
+
301
+ # If not in exons-only mode or no exons to process, return normal sequence
302
+ if 'sequence' in gene_data:
303
+ sequence = gene_data['sequence']
304
+ self.logger.debug(f"Got sequence of length: {len(sequence)}")
305
+
306
+ # Format sequence with padding in lowercase (only if not in exons-only mode)
307
+ if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
308
  padding = 30
309
+ start = gene_data['info']['start']
310
+ end = gene_data['info']['end']
311
  padded_start = max(0, start - padding)
312
  padded_end = min(len(sequence), end + padding)
313
 
 
318
 
319
  # Combine parts
320
  formatted_sequence = five_prime_pad + main_sequence + three_prime_pad
321
+ else:
322
+ formatted_sequence = sequence
323
+
324
+ result = {
325
+ 'sequence': formatted_sequence,
326
+ 'info': gene_data['info'],
327
+ 'start': gene_data['info']['start'],
328
+ 'end': gene_data['info']['end'],
329
+ 'full_location': gene_data['info'].get('full_location', '')
330
+ }
 
 
 
 
 
 
 
331
 
332
+ self.logger.debug(f"Returning sequence of length: {len(formatted_sequence)}")
333
+ return result
334
+
335
+ self.logger.warning(f"No sequence data found in gene_data for {identifier}")
336
+ return None
337
+
338
  except Exception as e:
339
  self.logger.error(f"Error getting gene sequence: {str(e)}")
340
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
 
349
  annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
350
  self.annotation_parser.set_annotation_file(annotation_path)
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  feature_info = {
353
+ 'chromosome': chrom, # Use raw chromosome ID directly
354
+ 'start': start-1, # Convert to 0-based indexing
355
  'end': end
356
  }
357
 
 
412
  if not gene_data or 'info' not in gene_data:
413
  self.logger.warning(f"No gene data found for identifier: {identifier}")
414
  return None
415
+
416
+ chrom = gene_data['info']['chromosome']
417
+
418
+ self.logger.debug(f"Getting sequence for chromosome: {chrom}, start: {start}, end: {end}")
419
 
420
  # Use _get_sequence_for_position to get sequence with padding
421
+ sequence = self._get_sequence_for_position(chrom, start, end)
422
  if sequence:
423
  result = {
424
  'sequence': sequence,
 
433
  except Exception as e:
434
  self.logger.error(f"Error getting gene sequence for range: {str(e)}")
435
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
436
+ return None
437
+
438
+ def set_view_exons_only(self, enabled):
439
+ """Set whether to view exons only"""
440
+ try:
441
+ self.logger.debug(f"Setting view exons only to: {enabled}")
442
+ self._view_exons_only = enabled
443
+ # Clear cache when changing view mode
444
+ self._sequence_cache.clear()
445
+ self.logger.debug("Cleared sequence cache")
446
+ except Exception as e:
447
+ self.logger.error(f"Error setting view exons only: {str(e)}")
448
+
449
+ def get_features_for_gene(self, locus_tag):
450
+ """Get features for a specific gene"""
451
+ try:
452
+ if not self.annotation_parser:
453
+ self._initialize_annotation_parser()
454
+
455
+ features = []
456
+ gene_data = self.get_gene_data(locus_tag)
457
+
458
+ if gene_data and 'info' in gene_data:
459
+ info = gene_data['info']
460
+
461
+ # Add the main gene feature
462
+ features.append({
463
+ 'type': info['feature_type'],
464
+ 'start': info['start'],
465
+ 'end': info['end'],
466
+ 'name': info['gene_name'],
467
+ 'id': locus_tag,
468
+ 'strand': '+' if '(+)' in info['location'] else '-'
469
+ })
470
+
471
+ # Parse additional features from full location if available
472
+ if 'full_location' in info and ',' in info['full_location']:
473
+ for i, part in enumerate(info['full_location'].split(',')):
474
+ coords = part.split('(')[0]
475
+ strand = part.split('(')[1][0]
476
+ start, end = map(int, coords.split('..'))
477
+
478
+ features.append({
479
+ 'type': 'exon',
480
+ 'start': start,
481
+ 'end': end,
482
+ 'name': f'Exon {i+1}',
483
+ 'id': f'{locus_tag}_exon_{i+1}',
484
+ 'strand': strand
485
+ })
486
+
487
+ return features
488
+
489
+ except Exception as e:
490
+ self.logger.error(f"Error getting features for gene: {str(e)}")
491
+ return []
492
+
493
+ def get_features_for_region(self, chromosome, start, end):
494
+ """Get features within a specific region"""
495
+ try:
496
+ if not self.annotation_parser:
497
+ self._initialize_annotation_parser()
498
+
499
+ features = []
500
+
501
+ # Search through index for features in this region
502
+ if hasattr(self, '_index') and 'locus_tags' in self._index:
503
+ for locus_tag, feature_info in self._index['locus_tags'].items():
504
+ if (feature_info['chromosome'] == chromosome and
505
+ feature_info['start'] <= end and
506
+ feature_info['end'] >= start):
507
+
508
+ features.append({
509
+ 'type': feature_info['feature_type'],
510
+ 'start': feature_info['start'],
511
+ 'end': feature_info['end'],
512
+ 'name': feature_info['gene_name'],
513
+ 'id': locus_tag,
514
+ 'strand': '+' if '(+)' in feature_info['location'] else '-'
515
+ })
516
+
517
+ return features
518
+
519
+ except Exception as e:
520
+ self.logger.error(f"Error getting features for region: {str(e)}")
521
+ return []
src/ui/ncbi.ui CHANGED
@@ -70,8 +70,8 @@
70
  <string>Step 1: Input Search Options</string>
71
  </property>
72
  <layout class="QGridLayout" name="gridLayout_4">
73
- <item row="3" column="0">
74
- <widget class="QLabel" name="lblCompleteGenomesOnly">
75
  <property name="sizePolicy">
76
  <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
77
  <horstretch>0</horstretch>
@@ -79,10 +79,10 @@
79
  </sizepolicy>
80
  </property>
81
  <property name="toolTip">
82
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Only display entries classified as &amp;quot;Complete Genomes&amp;quot; by NCBI.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
83
  </property>
84
  <property name="text">
85
- <string>Complete Genomes Only</string>
86
  </property>
87
  </widget>
88
  </item>
@@ -99,10 +99,10 @@
99
  </property>
100
  </widget>
101
  </item>
102
- <item row="2" column="1">
103
- <widget class="QLineEdit" name="ledMaxResults">
104
- <property name="text">
105
- <string>100</string>
106
  </property>
107
  </widget>
108
  </item>
@@ -119,15 +119,15 @@
119
  </property>
120
  </widget>
121
  </item>
122
- <item row="1" column="1">
123
- <widget class="QLineEdit" name="ledStrain">
124
  <property name="placeholderText">
125
- <string>Ex. K-12</string>
126
  </property>
127
  </widget>
128
  </item>
129
- <item row="2" column="0">
130
- <widget class="QLabel" name="lblMaxResults">
131
  <property name="sizePolicy">
132
  <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
133
  <horstretch>0</horstretch>
@@ -135,27 +135,37 @@
135
  </sizepolicy>
136
  </property>
137
  <property name="toolTip">
138
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Number of search results to return.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
139
  </property>
140
  <property name="text">
141
- <string>Max Results (Default = 100)</string>
142
  </property>
143
  </widget>
144
  </item>
145
- <item row="0" column="1">
146
- <widget class="QLineEdit" name="ledOrganism">
147
- <property name="placeholderText">
148
- <string>Ex. Escherichia coli</string>
149
  </property>
150
  </widget>
151
  </item>
152
- <item row="3" column="1">
153
- <widget class="QCheckBox" name="chkCompleteGenomesOnly">
154
  <property name="text">
155
- <string/>
 
 
 
 
 
 
 
156
  </property>
157
  </widget>
158
  </item>
 
 
 
159
  </layout>
160
  </widget>
161
  </item>
 
70
  <string>Step 1: Input Search Options</string>
71
  </property>
72
  <layout class="QGridLayout" name="gridLayout_4">
73
+ <item row="2" column="0">
74
+ <widget class="QLabel" name="lblMaxResults">
75
  <property name="sizePolicy">
76
  <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
77
  <horstretch>0</horstretch>
 
79
  </sizepolicy>
80
  </property>
81
  <property name="toolTip">
82
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Number of search results to return.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
83
  </property>
84
  <property name="text">
85
+ <string>Max Results (Default = 100)</string>
86
  </property>
87
  </widget>
88
  </item>
 
99
  </property>
100
  </widget>
101
  </item>
102
+ <item row="1" column="1">
103
+ <widget class="QLineEdit" name="ledStrain">
104
+ <property name="placeholderText">
105
+ <string>Ex. K-12</string>
106
  </property>
107
  </widget>
108
  </item>
 
119
  </property>
120
  </widget>
121
  </item>
122
+ <item row="0" column="1">
123
+ <widget class="QLineEdit" name="ledOrganism">
124
  <property name="placeholderText">
125
+ <string>Ex. Escherichia coli</string>
126
  </property>
127
  </widget>
128
  </item>
129
+ <item row="4" column="0">
130
+ <widget class="QLabel" name="lblCompleteGenomesOnly">
131
  <property name="sizePolicy">
132
  <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
133
  <horstretch>0</horstretch>
 
135
  </sizepolicy>
136
  </property>
137
  <property name="toolTip">
138
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Only display entries classified as &amp;quot;Complete Genomes&amp;quot; by NCBI.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
139
  </property>
140
  <property name="text">
141
+ <string>Complete Genomes Only</string>
142
  </property>
143
  </widget>
144
  </item>
145
+ <item row="4" column="1">
146
+ <widget class="QCheckBox" name="chkCompleteGenomesOnly">
147
+ <property name="text">
148
+ <string/>
149
  </property>
150
  </widget>
151
  </item>
152
+ <item row="2" column="1">
153
+ <widget class="QLineEdit" name="ledMaxResults">
154
  <property name="text">
155
+ <string>100</string>
156
+ </property>
157
+ </widget>
158
+ </item>
159
+ <item row="3" column="0">
160
+ <widget class="QLabel" name="lblDatabase">
161
+ <property name="text">
162
+ <string>Database:</string>
163
  </property>
164
  </widget>
165
  </item>
166
+ <item row="3" column="1">
167
+ <widget class="QComboBox" name="cmbDatabase"/>
168
+ </item>
169
  </layout>
170
  </widget>
171
  </item>
src/ui/view_targets.ui CHANGED
@@ -6,8 +6,8 @@
6
  <rect>
7
  <x>0</x>
8
  <y>0</y>
9
- <width>1335</width>
10
- <height>916</height>
11
  </rect>
12
  </property>
13
  <property name="font">
@@ -34,6 +34,16 @@
34
  <string>Gene Viewer</string>
35
  </property>
36
  <layout class="QGridLayout" name="gridLayout_6">
 
 
 
 
 
 
 
 
 
 
37
  <item row="2" column="0">
38
  <widget class="QLabel" name="lblStartLocation">
39
  <property name="text">
@@ -41,8 +51,8 @@
41
  </property>
42
  </widget>
43
  </item>
44
- <item row="2" column="1">
45
- <widget class="QLineEdit" name="ledStartLocation"/>
46
  </item>
47
  <item row="3" column="0">
48
  <widget class="QLabel" name="lblStopLocation">
@@ -51,16 +61,6 @@
51
  </property>
52
  </widget>
53
  </item>
54
- <item row="3" column="2">
55
- <widget class="QPushButton" name="pbtnResetLocation">
56
- <property name="text">
57
- <string>Reset Location</string>
58
- </property>
59
- </widget>
60
- </item>
61
- <item row="3" column="1">
62
- <widget class="QLineEdit" name="ledStopLocation"/>
63
- </item>
64
  <item row="2" column="2">
65
  <widget class="QPushButton" name="pbtnChangeLocation">
66
  <property name="toolTip">
@@ -74,6 +74,13 @@
74
  <item row="5" column="0" colspan="5">
75
  <widget class="QTextEdit" name="txtedGeneViewer"/>
76
  </item>
 
 
 
 
 
 
 
77
  </layout>
78
  </widget>
79
  </item>
@@ -89,23 +96,40 @@
89
  <string>Guide Viewer</string>
90
  </property>
91
  <layout class="QGridLayout" name="gridLayout_4">
92
- <item row="4" column="1" colspan="3">
93
- <widget class="QComboBox" name="cmbGene">
94
- <property name="minimumSize">
95
- <size>
96
- <width>225</width>
97
- <height>0</height>
98
- </size>
99
  </property>
100
  </widget>
101
  </item>
102
- <item row="10" column="2">
103
- <widget class="QPushButton" name="pbtnHighlightGuides">
104
  <property name="toolTip">
105
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This button will highlight the sequences in the Gene Viewer that match the sequences selected in the table.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
106
  </property>
107
  <property name="text">
108
- <string>Highlight Guides</string>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </property>
110
  </widget>
111
  </item>
@@ -119,29 +143,20 @@
119
  </property>
120
  </widget>
121
  </item>
122
- <item row="10" column="3">
123
- <widget class="QPushButton" name="pbtnClearGuides">
124
- <property name="toolTip">
125
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;This button clears all highlighted guides from the gene viewer box.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
126
- </property>
127
  <property name="text">
128
- <string>Clear Guides</string>
129
  </property>
130
  </widget>
131
  </item>
132
- <item row="6" column="2">
133
- <widget class="QLabel" name="lblMinOTScore">
134
  <property name="text">
135
- <string>Minimum On-Target Score</string>
136
  </property>
137
  </widget>
138
  </item>
139
- <item row="11" column="0" colspan="4">
140
- <widget class="QTableWidget" name="tblGuides"/>
141
- </item>
142
- <item row="6" column="3">
143
- <widget class="QSpinBox" name="spnMinOTScore"/>
144
- </item>
145
  <item row="5" column="0">
146
  <widget class="QLabel" name="lblEndonuclease">
147
  <property name="text">
@@ -149,27 +164,39 @@
149
  </property>
150
  </widget>
151
  </item>
152
- <item row="12" column="0">
153
- <widget class="QPushButton" name="pbtnScoringOptions">
 
 
 
 
 
 
154
  <property name="text">
155
- <string>Scoring Options</string>
156
  </property>
157
  </widget>
158
  </item>
159
- <item row="6" column="0">
160
- <widget class="QCheckBox" name="chkFilter5PrimeG">
161
- <property name="text">
162
- <string>Filter 5' G Sequences</string>
 
 
 
163
  </property>
164
  </widget>
165
  </item>
166
- <item row="10" column="0">
167
- <widget class="QCheckBox" name="chkSelectAll">
168
  <property name="text">
169
- <string>Select All</string>
170
  </property>
171
  </widget>
172
  </item>
 
 
 
173
  <item row="4" column="0">
174
  <widget class="QLabel" name="lblGene">
175
  <property name="text">
@@ -177,33 +204,13 @@
177
  </property>
178
  </widget>
179
  </item>
180
- <item row="12" column="1">
181
- <widget class="QPushButton" name="pbtnOffTarget">
182
- <property name="toolTip">
183
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Perform off-target analysis on the selected guides.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
184
- </property>
185
- <property name="text">
186
- <string>Off-Target</string>
187
- </property>
188
- </widget>
189
- </item>
190
- <item row="12" column="2">
191
- <widget class="QPushButton" name="pbtnCoTargeting">
192
- <property name="toolTip">
193
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Determine guides with synergistic PAMs (Ex. saCas9 and spCas9 compatible guides). &lt;span style=&quot; font-weight:600;&quot;&gt;Note:&lt;/span&gt; to analyze an organism for co-targeting guides, separate CSPR files must be generated for each additional endonuclease. Co-targeting endonucleases must have the same PAM directionality, same total gRNA length, and overlapping PAM sequences to be compatible.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
194
- </property>
195
- <property name="text">
196
- <string>Co-Targeting</string>
197
- </property>
198
- </widget>
199
- </item>
200
- <item row="12" column="3">
201
- <widget class="QPushButton" name="pbtnExportSelectedgRNAs">
202
  <property name="toolTip">
203
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Export selected guides to a CSV file.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
204
  </property>
205
  <property name="text">
206
- <string>Export Selected gRNAs</string>
207
  </property>
208
  </widget>
209
  </item>
 
6
  <rect>
7
  <x>0</x>
8
  <y>0</y>
9
+ <width>1920</width>
10
+ <height>1147</height>
11
  </rect>
12
  </property>
13
  <property name="font">
 
34
  <string>Gene Viewer</string>
35
  </property>
36
  <layout class="QGridLayout" name="gridLayout_6">
37
+ <item row="2" column="1">
38
+ <widget class="QLineEdit" name="ledStartLocation"/>
39
+ </item>
40
+ <item row="3" column="2">
41
+ <widget class="QPushButton" name="pbtnResetLocation">
42
+ <property name="text">
43
+ <string>Reset Location</string>
44
+ </property>
45
+ </widget>
46
+ </item>
47
  <item row="2" column="0">
48
  <widget class="QLabel" name="lblStartLocation">
49
  <property name="text">
 
51
  </property>
52
  </widget>
53
  </item>
54
+ <item row="3" column="1">
55
+ <widget class="QLineEdit" name="ledStopLocation"/>
56
  </item>
57
  <item row="3" column="0">
58
  <widget class="QLabel" name="lblStopLocation">
 
61
  </property>
62
  </widget>
63
  </item>
 
 
 
 
 
 
 
 
 
 
64
  <item row="2" column="2">
65
  <widget class="QPushButton" name="pbtnChangeLocation">
66
  <property name="toolTip">
 
74
  <item row="5" column="0" colspan="5">
75
  <widget class="QTextEdit" name="txtedGeneViewer"/>
76
  </item>
77
+ <item row="4" column="2">
78
+ <widget class="QCheckBox" name="chkViewExonsOnly">
79
+ <property name="text">
80
+ <string>View Exons Only</string>
81
+ </property>
82
+ </widget>
83
+ </item>
84
  </layout>
85
  </widget>
86
  </item>
 
96
  <string>Guide Viewer</string>
97
  </property>
98
  <layout class="QGridLayout" name="gridLayout_4">
99
+ <item row="12" column="2">
100
+ <widget class="QPushButton" name="pbtnCoTargeting">
101
+ <property name="toolTip">
102
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Determine guides with synergistic PAMs (Ex. saCas9 and spCas9 compatible guides). &lt;span style=&quot; font-weight:600;&quot;&gt;Note:&lt;/span&gt; to analyze an organism for co-targeting guides, separate CSPR files must be generated for each additional endonuclease. Co-targeting endonucleases must have the same PAM directionality, same total gRNA length, and overlapping PAM sequences to be compatible.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
103
+ </property>
104
+ <property name="text">
105
+ <string>Co-Targeting</string>
106
  </property>
107
  </widget>
108
  </item>
109
+ <item row="12" column="1">
110
+ <widget class="QPushButton" name="pbtnOffTarget">
111
  <property name="toolTip">
112
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Perform off-target analysis on the selected guides.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
113
  </property>
114
  <property name="text">
115
+ <string>Off-Target</string>
116
+ </property>
117
+ </widget>
118
+ </item>
119
+ <item row="12" column="3">
120
+ <widget class="QPushButton" name="pbtnExportSelectedgRNAs">
121
+ <property name="toolTip">
122
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Export selected guides to a CSV file.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
123
+ </property>
124
+ <property name="text">
125
+ <string>Export Selected gRNAs</string>
126
+ </property>
127
+ </widget>
128
+ </item>
129
+ <item row="10" column="0">
130
+ <widget class="QCheckBox" name="chkSelectAll">
131
+ <property name="text">
132
+ <string>Select All</string>
133
  </property>
134
  </widget>
135
  </item>
 
143
  </property>
144
  </widget>
145
  </item>
146
+ <item row="6" column="0">
147
+ <widget class="QCheckBox" name="chkFilter5PrimeG">
 
 
 
148
  <property name="text">
149
+ <string>Filter 5' G Sequences</string>
150
  </property>
151
  </widget>
152
  </item>
153
+ <item row="12" column="0">
154
+ <widget class="QPushButton" name="pbtnScoringOptions">
155
  <property name="text">
156
+ <string>Scoring Options</string>
157
  </property>
158
  </widget>
159
  </item>
 
 
 
 
 
 
160
  <item row="5" column="0">
161
  <widget class="QLabel" name="lblEndonuclease">
162
  <property name="text">
 
164
  </property>
165
  </widget>
166
  </item>
167
+ <item row="6" column="3">
168
+ <widget class="QSpinBox" name="spnMinOTScore"/>
169
+ </item>
170
+ <item row="10" column="2">
171
+ <widget class="QPushButton" name="pbtnHighlightGuides">
172
+ <property name="toolTip">
173
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This button will highlight the sequences in the Gene Viewer that match the sequences selected in the table.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
174
+ </property>
175
  <property name="text">
176
+ <string>Highlight Guides</string>
177
  </property>
178
  </widget>
179
  </item>
180
+ <item row="4" column="1" colspan="3">
181
+ <widget class="QComboBox" name="cmbGene">
182
+ <property name="minimumSize">
183
+ <size>
184
+ <width>225</width>
185
+ <height>0</height>
186
+ </size>
187
  </property>
188
  </widget>
189
  </item>
190
+ <item row="6" column="2">
191
+ <widget class="QLabel" name="lblMinOTScore">
192
  <property name="text">
193
+ <string>Minimum On-Target Score</string>
194
  </property>
195
  </widget>
196
  </item>
197
+ <item row="11" column="0" colspan="4">
198
+ <widget class="QTableWidget" name="tblGuides"/>
199
+ </item>
200
  <item row="4" column="0">
201
  <widget class="QLabel" name="lblGene">
202
  <property name="text">
 
204
  </property>
205
  </widget>
206
  </item>
207
+ <item row="10" column="3">
208
+ <widget class="QPushButton" name="pbtnClearGuides">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  <property name="toolTip">
210
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;This button clears all highlighted guides from the gene viewer box.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
211
  </property>
212
  <property name="text">
213
+ <string>Clear Guides</string>
214
  </property>
215
  </widget>
216
  </item>
src/views/CloseableTabWidget.py CHANGED
@@ -65,8 +65,8 @@ class CloseableTabWidget(QTabWidget):
65
  # Add the tab
66
  index = super().addTab(widget, label)
67
 
68
- if index != 0:
69
- # Create and setup close button
70
  close_button = self._create_close_button(index, label)
71
  self._tabs[tab_id]['close_button'] = close_button
72
  self.tabBar().setTabButton(index, QTabBar.ButtonPosition.RightSide, close_button)
@@ -78,25 +78,37 @@ class CloseableTabWidget(QTabWidget):
78
 
79
  def _create_close_button(self, index, label):
80
  """Create a new close button for a tab"""
81
- close_button = QToolButton(self.tabBar())
82
- close_button.setObjectName(f"close_button_{label}")
83
- close_icon = self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_TitleBarCloseButton)
84
- close_button.setIcon(close_icon)
85
- close_button.setIconSize(QSize(16, 16))
86
- close_button.setAutoRaise(True)
87
- close_button.setStyleSheet("""
88
- QToolButton {
89
- border: none;
90
- padding: 0px;
91
- }
92
- QToolButton:hover {
93
- background: #c42b1c;
94
- }
95
- """)
96
- close_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
97
- close_button.setFixedSize(18, 18)
98
- close_button.clicked.connect(lambda checked, idx=index: self.safely_close_tab(idx))
99
- return close_button
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  def safely_close_tab(self, index):
102
  """Safely handle tab closure with error checking"""
@@ -118,14 +130,15 @@ class CloseableTabWidget(QTabWidget):
118
  def _update_all_tabs(self):
119
  """Update all tabs and their close buttons"""
120
  try:
121
- for i in range(1, self.count()): # Skip index 0 (home tab)
122
  widget = self.widget(i)
123
  if widget:
124
  label = self.tabText(i)
125
  tab_id = f"{label}_{id(widget)}"
126
 
127
- # Create new close button if needed
128
- if tab_id not in self._tabs or not self._tabs[tab_id].get('close_button'):
 
129
  close_button = self._create_close_button(i, label)
130
  self._tabs[tab_id] = {
131
  'widget': widget,
@@ -133,7 +146,7 @@ class CloseableTabWidget(QTabWidget):
133
  'close_button': close_button
134
  }
135
  self.tabBar().setTabButton(i, QTabBar.ButtonPosition.RightSide, close_button)
136
- else:
137
  # Update existing close button's click connection
138
  close_button = self._tabs[tab_id]['close_button']
139
  close_button.clicked.disconnect()
 
65
  # Add the tab
66
  index = super().addTab(widget, label)
67
 
68
+ # Create and setup close button for all tabs except Home and Startup
69
+ if label not in ["Home", "Startup"]:
70
  close_button = self._create_close_button(index, label)
71
  self._tabs[tab_id]['close_button'] = close_button
72
  self.tabBar().setTabButton(index, QTabBar.ButtonPosition.RightSide, close_button)
 
78
 
79
  def _create_close_button(self, index, label):
80
  """Create a new close button for a tab"""
81
+ try:
82
+ close_button = QToolButton(self.tabBar())
83
+ close_button.setObjectName(f"close_button_{label}")
84
+
85
+ # Always use a custom close icon style
86
+ close_button.setText("×") # Using multiplication symbol as close icon
87
+ close_button.setStyleSheet("""
88
+ QToolButton {
89
+ border: none;
90
+ padding: 0px;
91
+ color: #666666;
92
+ background: transparent;
93
+ font-size: 16px;
94
+ font-weight: bold;
95
+ }
96
+ QToolButton:hover {
97
+ color: #ffffff;
98
+ background: #c42b1c;
99
+ }
100
+ """)
101
+
102
+ close_button.setAutoRaise(True)
103
+ close_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
104
+ close_button.setFixedSize(20, 20)
105
+ close_button.clicked.connect(lambda checked, idx=index: self.safely_close_tab(idx))
106
+
107
+ return close_button
108
+
109
+ except Exception as e:
110
+ self.logger.error(f"Error creating close button: {e}")
111
+ raise
112
 
113
  def safely_close_tab(self, index):
114
  """Safely handle tab closure with error checking"""
 
130
  def _update_all_tabs(self):
131
  """Update all tabs and their close buttons"""
132
  try:
133
+ for i in range(self.count()):
134
  widget = self.widget(i)
135
  if widget:
136
  label = self.tabText(i)
137
  tab_id = f"{label}_{id(widget)}"
138
 
139
+ # Create new close button if needed and if not Home or Startup tab
140
+ if (label not in ["Home", "Startup"] and
141
+ (tab_id not in self._tabs or not self._tabs[tab_id].get('close_button'))):
142
  close_button = self._create_close_button(i, label)
143
  self._tabs[tab_id] = {
144
  'widget': widget,
 
146
  'close_button': close_button
147
  }
148
  self.tabBar().setTabButton(i, QTabBar.ButtonPosition.RightSide, close_button)
149
+ elif label not in ["Home", "Startup"] and tab_id in self._tabs:
150
  # Update existing close button's click connection
151
  close_button = self._tabs[tab_id]['close_button']
152
  close_button.clicked.disconnect()
src/views/DNAFeatureViewer.py ADDED
@@ -0,0 +1,1227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
2
+ QGraphicsObject, QGraphicsSimpleTextItem, QApplication,
3
+ QLabel, QFrame, QGraphicsLineItem)
4
+ from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF, QLineF, QSizeF, QTimer
5
+ from PyQt6.QtGui import QPainter, QPen, QBrush, QColor, QPainterPath, QFont, QPolygonF, QTransform, QKeySequence
6
+
7
+ class DNAFeatureViewer(QWidget): # Change to QWidget
8
+ """Custom widget for displaying DNA features with sequence"""
9
+ sequence_selected = pyqtSignal(int, int) # Emit start and end positions when sequence is selected
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent)
13
+
14
+ # Add logger
15
+ import logging
16
+ self.logger = logging.getLogger(__name__)
17
+
18
+ # Create layout for this widget
19
+ self.layout = QVBoxLayout(self)
20
+ self.layout.setContentsMargins(0, 0, 0, 0)
21
+ self.layout.setSpacing(0)
22
+
23
+ # Create graphics view with left alignment
24
+ self.view = QGraphicsView()
25
+ self.scene = QGraphicsScene(self)
26
+ self.view.setScene(self.scene)
27
+ self.view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
28
+
29
+ # Create components
30
+ self.sequence_viewer = SequenceViewer()
31
+ self.feature_viewer = FeatureViewer()
32
+
33
+ # Add components to scene
34
+ self.scene.addItem(self.sequence_viewer)
35
+ self.scene.addItem(self.feature_viewer)
36
+
37
+ # Connect signals from both viewers
38
+ self.sequence_viewer.sequence_selected.connect(self._on_sequence_selected)
39
+ self.sequence_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
40
+ self.feature_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
41
+
42
+ # Create status panel
43
+ self.status_panel = QLabel()
44
+ self.status_panel.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken)
45
+ self.status_panel.setStyleSheet("""
46
+ QLabel {
47
+ background-color: #f0f0f0;
48
+ padding: 5px;
49
+ border-top: 1px solid #ccc;
50
+ min-height: 20px;
51
+ }
52
+ """)
53
+
54
+ # Add widgets to layout
55
+ self.layout.addWidget(self.view)
56
+ self.layout.addWidget(self.status_panel)
57
+
58
+ # Setup view
59
+ self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
60
+ self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
61
+ self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
62
+ self.view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
63
+ self.view.setMinimumHeight(200)
64
+
65
+ # Add resize event handling
66
+ self.view.viewport().installEventFilter(self)
67
+
68
+ # Add resize timer for debouncing
69
+ self.resize_timer = QTimer()
70
+ self.resize_timer.setSingleShot(True)
71
+ self.resize_timer.timeout.connect(self._delayed_resize)
72
+ self.cached_size = None
73
+
74
+ # Create ruler view with left alignment
75
+ self.ruler_view = QGraphicsView()
76
+ self.ruler_scene = QGraphicsScene(self)
77
+ self.ruler_view.setScene(self.ruler_scene)
78
+ self.ruler_view.setFixedHeight(25)
79
+ self.ruler_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
80
+ self.ruler_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
81
+ self.ruler_view.setViewportMargins(0, 0, 0, 0)
82
+ self.ruler_view.setFrameStyle(0)
83
+ self.ruler_view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
84
+
85
+ # Set opaque white background to hide sequence behind ruler
86
+ self.ruler_view.setBackgroundBrush(QBrush(Qt.GlobalColor.white))
87
+ self.ruler_view.setAutoFillBackground(True)
88
+
89
+ # Add ruler to layout before main view
90
+ self.layout.insertWidget(0, self.ruler_view)
91
+
92
+ # Connect horizontal scroll bars
93
+ self.view.horizontalScrollBar().valueChanged.connect(
94
+ self.ruler_view.horizontalScrollBar().setValue
95
+ )
96
+
97
+ # Create initial ruler
98
+ self._create_ruler()
99
+
100
+ def eventFilter(self, obj, event):
101
+ """Handle viewport resize events with debouncing"""
102
+ if obj == self.view.viewport() and event.type() == event.Type.Resize:
103
+ # Cache the new size
104
+ self.cached_size = event.size()
105
+ # Reset and start the timer
106
+ self.resize_timer.stop()
107
+ self.resize_timer.start(150) # 150ms delay
108
+ return super().eventFilter(obj, event)
109
+
110
+ def _delayed_resize(self):
111
+ """Handle resize after debouncing period"""
112
+ if self.cached_size:
113
+ try:
114
+ self.view.setUpdatesEnabled(False)
115
+ self.sequence_viewer.setVisible(False)
116
+ self.feature_viewer.setVisible(False)
117
+
118
+ self._handle_resize(self.cached_size)
119
+
120
+ # Call reapply_highlights on sequence_viewer instead of self
121
+ self.sequence_viewer._reapply_highlights()
122
+
123
+ self.sequence_viewer.setVisible(True)
124
+ self.feature_viewer.setVisible(True)
125
+ self.view.setUpdatesEnabled(True)
126
+
127
+ # Force immediate update
128
+ self.view.viewport().update()
129
+
130
+ except Exception as e:
131
+ self.logger.error(f"Error in delayed resize: {str(e)}")
132
+ finally:
133
+ self.view.setUpdatesEnabled(True)
134
+ self.sequence_viewer.setVisible(True)
135
+ self.feature_viewer.setVisible(True)
136
+
137
+ def _handle_resize(self, new_size):
138
+ """Handle viewport resize by adjusting sequence display"""
139
+ try:
140
+ viewport_width = max(1, new_size.width())
141
+ margin = 100 # Space for position numbers
142
+
143
+ # Calculate available width for bases
144
+ available_width = viewport_width - margin
145
+ base_width = self.sequence_viewer.base_width
146
+
147
+ # Calculate maximum number of bases that can fit
148
+ max_bases = (available_width // base_width)
149
+
150
+ # Round down to nearest multiple of 10
151
+ new_bases = (max_bases // 10) * 10
152
+
153
+ # Ensure minimum of 10 bases
154
+ new_bases = max(10, new_bases)
155
+
156
+ # Calculate total width needed
157
+ total_width = (new_bases * base_width) + margin
158
+
159
+ # Only update sequence if bases per line needs to change
160
+ if new_bases != self.sequence_viewer.bases_per_line:
161
+ self.logger.debug(
162
+ f"Resizing from {self.sequence_viewer.bases_per_line} to {new_bases} bases per line"
163
+ )
164
+
165
+ # Batch update both viewers
166
+ self.view.setUpdatesEnabled(False)
167
+ self.ruler_view.setUpdatesEnabled(False)
168
+
169
+ # Update sequence viewer
170
+ self.sequence_viewer.bases_per_line = new_bases
171
+ self.sequence_viewer._create_nucleotide_items()
172
+
173
+ # Update feature viewer to match
174
+ if hasattr(self, 'feature_viewer'):
175
+ self.feature_viewer.bases_per_line = new_bases
176
+ self.feature_viewer.update()
177
+
178
+ # Calculate total height needed
179
+ sequence_length = len(self.sequence_viewer.sequence)
180
+ total_lines = (sequence_length + new_bases - 1) // new_bases
181
+ total_height = total_lines * self.sequence_viewer.line_spacing
182
+
183
+ # Set fixed scene rect with left alignment and full height
184
+ scene_rect = QRectF(0, 0, total_width, total_height)
185
+ self.scene.setSceneRect(scene_rect)
186
+
187
+ # Update ruler with exact viewport width
188
+ self._create_ruler()
189
+ self.ruler_view.setFixedWidth(viewport_width + 10)
190
+ # Set ruler scene rect to exactly match viewport width
191
+ self.ruler_scene.setSceneRect(0, 0, viewport_width + 10, 25)
192
+
193
+ # Keep scroll positions in sync
194
+ scroll_value = self.view.horizontalScrollBar().value()
195
+ self.ruler_view.horizontalScrollBar().setValue(scroll_value)
196
+
197
+ # Ensure view shows full content
198
+ self.view.setSceneRect(scene_rect)
199
+
200
+ self.view.setUpdatesEnabled(True)
201
+ self.ruler_view.setUpdatesEnabled(True)
202
+
203
+ # Force immediate update
204
+ self.view.viewport().update()
205
+ self.ruler_view.viewport().update()
206
+ else:
207
+ # Even if we don't resize, ensure proper alignment
208
+ self.ruler_view.setFixedWidth(viewport_width + 10)
209
+ # Update ruler scene rect to match viewport exactly
210
+ self.ruler_scene.setSceneRect(0, 0, viewport_width + 10, 25)
211
+ scroll_value = self.view.horizontalScrollBar().value()
212
+ self.ruler_view.horizontalScrollBar().setValue(scroll_value)
213
+ self.ruler_view.viewport().update()
214
+
215
+ except Exception as e:
216
+ self.logger.error(f"Error in _handle_resize: {str(e)}")
217
+
218
+ def set_data(self, sequence, features, start_pos=None):
219
+ """Set data for both viewers"""
220
+ if start_pos is None:
221
+ start_pos = 0
222
+
223
+ # Update both components
224
+ self.sequence_viewer.set_data(sequence, start_pos)
225
+ self.feature_viewer.set_data(sequence, features, start_pos)
226
+
227
+ # Position feature viewer to overlap with sequence viewer
228
+ self.feature_viewer.setY(0)
229
+
230
+ # Update scene rect to encompass both viewers
231
+ combined_rect = self.sequence_viewer.boundingRect().united(self.feature_viewer.boundingRect())
232
+ self.scene.setSceneRect(combined_rect)
233
+
234
+ # Update status panel with initial sequence info
235
+ sequence_length = len(sequence)
236
+ self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
237
+
238
+ self.update()
239
+
240
+ def _on_sequence_selected(self, start_pos, end_pos):
241
+ """Handle sequence selection"""
242
+ # Calculate selected length
243
+ selected_length = end_pos - start_pos + 1
244
+
245
+ # Update status panel with selection info
246
+ self.status_panel.setText(f"Selected: {start_pos}...{end_pos} = {selected_length} bp")
247
+
248
+ # Emit signal for other components
249
+ self.sequence_selected.emit(start_pos, end_pos)
250
+
251
+ def clear_selection(self):
252
+ """Clear selection and reset status panel"""
253
+ if hasattr(self, 'sequence_viewer'):
254
+ sequence = self.sequence_viewer.sequence
255
+ start_pos = self.sequence_viewer.start_pos
256
+ sequence_length = len(sequence)
257
+ self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
258
+
259
+ def _on_cursor_position_changed(self, position):
260
+ """Handle cursor position changes"""
261
+ if position >= 0:
262
+ self.status_panel.setText(f"Insertion Point: {position}")
263
+ else:
264
+ # Reset to showing current sequence range if cursor position is invalid
265
+ if hasattr(self, 'sequence_viewer'):
266
+ sequence = self.sequence_viewer.sequence
267
+ start_pos = self.sequence_viewer.start_pos
268
+ sequence_length = len(sequence)
269
+ self.status_panel.setText(f"Showing: {start_pos}...{start_pos + sequence_length} = {sequence_length} bp")
270
+
271
+ def _create_ruler(self):
272
+ """Create ruler with position markers"""
273
+ try:
274
+ self.ruler_scene.clear()
275
+
276
+ # Get current bases per line
277
+ bases_per_line = self.sequence_viewer.bases_per_line
278
+ base_width = self.sequence_viewer.base_width
279
+
280
+ # Calculate total width including margin
281
+ total_width = bases_per_line * base_width + 100 # Match sequence viewer width
282
+
283
+ # Create horizontal blue line aligned with sequence
284
+ ruler_line = QGraphicsLineItem(0, 15, bases_per_line * base_width, 15)
285
+ ruler_line.setPen(QPen(QColor(0, 120, 215), 1))
286
+ self.ruler_scene.addItem(ruler_line)
287
+
288
+ # Add tick marks and numbers for every base
289
+ for i in range(0, bases_per_line):
290
+ x_pos = i * base_width + base_width/2 # Center tick marks between bases
291
+
292
+ # Use 1-based indexing for position calculation
293
+ pos_1_based = i + 1
294
+
295
+ # Determine tick height based on position
296
+ if pos_1_based % 10 == 0: # Major ticks (every 10)
297
+ tick_height = 10
298
+ tick_start = 10
299
+ # Add number
300
+ text = QGraphicsSimpleTextItem(str(pos_1_based))
301
+ text.setFont(QFont("Arial", 8))
302
+ text_width = text.boundingRect().width()
303
+ text.setPos(x_pos - text_width/2, 0) # Position above line
304
+ self.ruler_scene.addItem(text)
305
+ elif pos_1_based % 5 == 0: # Medium ticks (every 5)
306
+ tick_height = 7
307
+ tick_start = 11
308
+ else: # Small ticks (every 1)
309
+ tick_height = 4
310
+ tick_start = 13
311
+
312
+ # Create tick mark
313
+ tick = QGraphicsLineItem(x_pos, tick_start, x_pos, tick_start + tick_height)
314
+ tick.setPen(QPen(QColor(0, 120, 215), 1))
315
+ self.ruler_scene.addItem(tick)
316
+
317
+ # Set scene rect to exactly match sequence viewer width
318
+ self.ruler_scene.setSceneRect(0, 0, total_width, 25)
319
+
320
+ except Exception as e:
321
+ self.logger.error(f"Error creating ruler: {str(e)}")
322
+
323
+ class NucleotideItem(QGraphicsObject):
324
+ clicked = pyqtSignal(object)
325
+
326
+ def __init__(self, nucleotide, x, y, width, is_uppercase=False, is_complement=False, parent=None):
327
+ super().__init__(parent)
328
+ self.nucleotide = nucleotide
329
+ self.spacing = 0
330
+ self.base_width = width
331
+ self.rect = QRectF(0, 0, width, width * 2)
332
+ self.setPos(x, y)
333
+ self.is_uppercase = is_uppercase
334
+ self.is_complement = is_complement
335
+ self.is_highlighted = False
336
+ self.highlight_color = None
337
+ self.show_cursor = False
338
+ self.cursor_side = 'right'
339
+ self.setAcceptHoverEvents(True)
340
+
341
+ # Get logger from parent
342
+ sequence_viewer = self.parent()
343
+ if sequence_viewer and hasattr(sequence_viewer, 'logger'):
344
+ self.logger = sequence_viewer.logger
345
+ else:
346
+ import logging
347
+ self.logger = logging.getLogger(__name__)
348
+
349
+ def boundingRect(self):
350
+ return self.rect
351
+
352
+ def paint(self, painter, option, widget):
353
+ try:
354
+ # Draw highlight background if highlighted
355
+ if self.is_highlighted and self.highlight_color:
356
+ painter.fillRect(self.rect, self.highlight_color)
357
+
358
+ # Draw nucleotide
359
+ painter.setFont(QFont("Courier", 12))
360
+ if self.is_uppercase:
361
+ painter.setPen(Qt.GlobalColor.black)
362
+ else:
363
+ painter.setPen(QColor(100, 100, 100))
364
+
365
+ # Get complement nucleotide if needed
366
+ display_nucleotide = self.nucleotide
367
+ if self.is_complement:
368
+ complement_map = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
369
+ 'a': 't', 't': 'a', 'g': 'c', 'c': 'g'}
370
+ display_nucleotide = complement_map.get(self.nucleotide, self.nucleotide)
371
+
372
+ # Draw text centered
373
+ painter.drawText(self.rect, Qt.AlignmentFlag.AlignCenter, display_nucleotide)
374
+
375
+ # Draw cursor if active
376
+ if self.show_cursor:
377
+ painter.setPen(QPen(Qt.GlobalColor.black, 1))
378
+ if self.cursor_side == 'right':
379
+ x = self.rect.right() - 1
380
+ else:
381
+ x = self.rect.left() + 1
382
+ cursor_height = self.rect.height()
383
+ painter.drawLine(QPointF(x, 0), QPointF(x, cursor_height))
384
+
385
+ except Exception as e:
386
+ self.logger.error(f"Error in paint: {str(e)}")
387
+
388
+ def mousePressEvent(self, event):
389
+ try:
390
+ if event.button() == Qt.MouseButton.LeftButton:
391
+ sequence_viewer = self.parent()
392
+ if sequence_viewer:
393
+ pos = sequence_viewer.get_nucleotide_position(self)
394
+ local_x = event.pos().x()
395
+
396
+ # Determine cursor position based on click location
397
+ is_right_side = local_x > self.rect.width() / 2
398
+
399
+ # Always show cursor
400
+ self.show_cursor = True
401
+ self.cursor_side = 'right' if is_right_side else 'left'
402
+
403
+ # Calculate cursor position
404
+ cursor_pos = pos + 1 if is_right_side else pos
405
+
406
+ self.logger.debug(f"""
407
+ Mouse click details:
408
+ - Nucleotide: {self.nucleotide}
409
+ - Click X: {local_x}
410
+ - Is right side: {is_right_side}
411
+ - Cursor position: {cursor_pos}
412
+ """)
413
+
414
+ # Start selection
415
+ sequence_viewer.selection_active = True
416
+ sequence_viewer.selection_start = cursor_pos
417
+ sequence_viewer.selection_end = cursor_pos
418
+
419
+ # Clear other cursors
420
+ for nuc in sequence_viewer.nucleotides:
421
+ if nuc != self:
422
+ nuc.show_cursor = False
423
+ nuc.update()
424
+
425
+ sequence_viewer.cursor_position_changed.emit(cursor_pos)
426
+ sequence_viewer._update_selection()
427
+ self.update()
428
+
429
+ except Exception as e:
430
+ self.logger.error(f"Error in mousePressEvent: {str(e)}")
431
+
432
+ event.accept()
433
+
434
+ def mouseMoveEvent(self, event):
435
+ sequence_viewer = self.parent()
436
+ if sequence_viewer and event.buttons() & Qt.MouseButton.LeftButton:
437
+ try:
438
+ pos = sequence_viewer.get_nucleotide_position(self)
439
+ local_x = event.pos().x()
440
+
441
+ # Calculate position relative to letter boundaries
442
+ text_x = (self.rect.width() - self.base_width) / 2
443
+ relative_x = local_x - text_x
444
+
445
+ # Force cursor to show
446
+ self.show_cursor = True
447
+
448
+ # Determine cursor position
449
+ if relative_x <= 0: # Before letter
450
+ self.cursor_side = 'left'
451
+ cursor_pos = pos
452
+ elif relative_x >= self.base_width: # After letter
453
+ self.cursor_side = 'right'
454
+ cursor_pos = pos + 1
455
+ else: # On letter
456
+ is_after = relative_x > self.base_width / 2
457
+ self.cursor_side = 'right' if is_after else 'left'
458
+ cursor_pos = pos + 1 if is_after else pos
459
+
460
+ # Update selection
461
+ sequence_viewer.selection_end = pos
462
+
463
+ # Clear other cursors
464
+ for nuc in sequence_viewer.nucleotides:
465
+ if nuc != self:
466
+ nuc.show_cursor = False
467
+ nuc.update()
468
+
469
+ sequence_viewer.cursor_position_changed.emit(cursor_pos)
470
+ sequence_viewer._update_selection()
471
+ self.update() # Force redraw
472
+
473
+ except Exception as e:
474
+ self.logger.error(f"Error in mouseMoveEvent: {str(e)}")
475
+
476
+ event.accept()
477
+
478
+ def hoverMoveEvent(self, event):
479
+ local_x = event.pos().x()
480
+ mid_point = self.rect.width() / 2
481
+
482
+ sequence_viewer = self.parent()
483
+ if sequence_viewer:
484
+ # Only show cursor if not selecting
485
+ if not sequence_viewer.selection_active:
486
+ self.show_cursor = True
487
+ self.cursor_side = 'right' if local_x >= mid_point else 'left'
488
+
489
+ # Clear cursor from other nucleotides
490
+ for nuc in sequence_viewer.nucleotides:
491
+ if nuc != self:
492
+ nuc.show_cursor = False
493
+ nuc.update()
494
+
495
+ pos = sequence_viewer.get_nucleotide_position(self)
496
+ cursor_pos = pos + 1 if self.cursor_side == 'right' else pos
497
+ sequence_viewer.cursor_position_changed.emit(cursor_pos)
498
+ self.update()
499
+
500
+ def hoverLeaveEvent(self, event):
501
+ sequence_viewer = self.parent()
502
+ if sequence_viewer and not sequence_viewer.selection_active:
503
+ self.show_cursor = False
504
+ self.update()
505
+ super().hoverLeaveEvent(event)
506
+
507
+ class SequenceViewer(QGraphicsObject):
508
+ sequence_selected = pyqtSignal(int, int)
509
+ cursor_position_changed = pyqtSignal(int) # New signal for cursor position
510
+
511
+ def __init__(self, parent=None):
512
+ super().__init__(parent)
513
+ self.sequence = ""
514
+ self.start_pos = 0
515
+ self.base_width = 15
516
+ self.bases_per_line = 70
517
+ self.line_height = 25
518
+ self.line_spacing = 80 # Increased spacing between gene lines
519
+ self.nucleotides = []
520
+ self.selection_start = None
521
+ self.selection_end = None
522
+ self.setAcceptHoverEvents(True)
523
+
524
+ # Add clipboard support
525
+ self.clipboard = QApplication.clipboard()
526
+
527
+ # Add tracking for drag start position
528
+ self.drag_start_pos = None
529
+ self.selection_active = False
530
+
531
+ # Enable mouse tracking
532
+ self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
533
+ self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsFocusable)
534
+
535
+ # Add logger
536
+ import logging
537
+ self.logger = logging.getLogger(__name__)
538
+
539
+ # Configure logging
540
+ handler = logging.StreamHandler()
541
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
542
+ handler.setFormatter(formatter)
543
+ self.logger.addHandler(handler)
544
+ self.logger.setLevel(logging.DEBUG)
545
+
546
+ # Add highlight state tracking
547
+ self.highlighted_regions = [] # List of (start, end, color, strand) tuples
548
+
549
+ def _get_text_width(self):
550
+ """Calculate text width based on current bases per line"""
551
+ return self.base_width * self.bases_per_line
552
+
553
+ def set_data(self, sequence, start_pos):
554
+ self.sequence = sequence
555
+ self.start_pos = start_pos
556
+ self._create_nucleotide_items()
557
+ self.update()
558
+
559
+ def _create_nucleotide_items(self):
560
+ try:
561
+ # Get the view from the scene
562
+ view = None
563
+ if self.scene():
564
+ views = self.scene().views()
565
+ if views:
566
+ view = views[0]
567
+ view.setUpdatesEnabled(False) # Use view instead of scene
568
+
569
+ # Clear existing items
570
+ self.cleanup_graphics()
571
+
572
+ current_pos = 0
573
+ max_width = 0 # Track maximum line width
574
+
575
+ # Pre-calculate total lines
576
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
577
+
578
+ # Pre-allocate lists for better performance
579
+ self.plot_lines = []
580
+ self.tick_lines = []
581
+ self.nucleotides = []
582
+
583
+ # Track nucleotides by strand and line
584
+ self.nucleotide_map = {
585
+ '+': [], # List of lists for positive strand nucleotides by line
586
+ '-': [] # List of lists for negative strand nucleotides by line
587
+ }
588
+ current_line = 0
589
+ self.nucleotide_map['+'].append([]) # Initialize first line
590
+ self.nucleotide_map['-'].append([])
591
+
592
+ # Create nucleotides and setup display
593
+ self._create_display()
594
+
595
+ # Reapply highlights after creating nucleotides
596
+ self._reapply_highlights()
597
+
598
+ except Exception as e:
599
+ self.logger.error(f"Error in _create_nucleotide_items: {str(e)}")
600
+ # Make sure we re-enable updates even if there's an error
601
+ if view:
602
+ view.setUpdatesEnabled(True)
603
+
604
+ def _create_display(self):
605
+ """Create the nucleotide display"""
606
+ try:
607
+ current_pos = 0
608
+ max_width = 0 # Add max_width definition here
609
+
610
+ while current_pos < len(self.sequence):
611
+ # Calculate exact number of bases for this line
612
+ remaining_bases = len(self.sequence) - current_pos
613
+ bases_this_line = min(self.bases_per_line, remaining_bases)
614
+ line_text = self.sequence[current_pos:current_pos + bases_this_line]
615
+
616
+ line_num = current_pos // self.bases_per_line
617
+ y_pos = line_num * self.line_spacing
618
+
619
+ # Calculate width for position numbers
620
+ max_width = max(max_width, self.bases_per_line * self.base_width)
621
+
622
+ # Create positive strand nucleotides
623
+ line_nucleotides_pos = []
624
+ for i, nucleotide in enumerate(line_text):
625
+ x_pos = i * self.base_width
626
+ nuc_item = NucleotideItem(
627
+ nucleotide=nucleotide,
628
+ x=x_pos,
629
+ y=y_pos + self.line_height * -0.3,
630
+ width=self.base_width,
631
+ is_uppercase=nucleotide.isupper(),
632
+ parent=self
633
+ )
634
+ self.nucleotides.append(nuc_item)
635
+ line_nucleotides_pos.append(nuc_item)
636
+
637
+ # Create complement strand nucleotides
638
+ line_nucleotides_neg = []
639
+ for i, nucleotide in enumerate(line_text):
640
+ x_pos = i * self.base_width
641
+ nuc_item = NucleotideItem(
642
+ nucleotide=nucleotide,
643
+ x=x_pos,
644
+ y=y_pos + self.line_height * 1.1,
645
+ width=self.base_width,
646
+ is_uppercase=nucleotide.isupper(),
647
+ is_complement=True,
648
+ parent=self
649
+ )
650
+ self.nucleotides.append(nuc_item)
651
+ line_nucleotides_neg.append(nuc_item)
652
+
653
+ # Store nucleotides by strand and line
654
+ if line_num >= len(self.nucleotide_map['+']):
655
+ self.nucleotide_map['+'].append([])
656
+ self.nucleotide_map['-'].append([])
657
+ self.nucleotide_map['+'][line_num].extend(line_nucleotides_pos)
658
+ self.nucleotide_map['-'][line_num].extend(line_nucleotides_neg)
659
+
660
+ # Draw plot line matching exactly the sequence width for this line
661
+ plot_y = y_pos + self.line_height
662
+ plot_line = QGraphicsLineItem(0, plot_y,
663
+ bases_this_line * self.base_width, plot_y, self)
664
+ plot_line.setPen(QPen(Qt.GlobalColor.black, 1))
665
+ self.plot_lines.append(plot_line)
666
+
667
+ # Draw tick marks only for actual bases in this line
668
+ for i in range(bases_this_line):
669
+ x_pos = i * self.base_width
670
+
671
+ # Convert to 1-based index for position calculation
672
+ pos_1_based = current_pos + i + 1 # Add 1 for 1-based indexing
673
+
674
+ # Determine tick height based on position
675
+ if i == 0 and current_pos == 0: # First base
676
+ tick_height = 12 # Longest tick for start
677
+ elif i == bases_this_line - 1 and current_pos + bases_this_line == len(self.sequence): # Last base
678
+ tick_height = 12 # Longest tick for end
679
+ elif pos_1_based % 10 == 0: # Major ticks (every 10)
680
+ tick_height = 10
681
+ elif pos_1_based % 5 == 0: # Medium ticks (every 5)
682
+ tick_height = 8
683
+ else: # Regular ticks
684
+ tick_height = 5
685
+
686
+ tick_line = QGraphicsLineItem(
687
+ x_pos + self.base_width/2,
688
+ plot_y - tick_height/2,
689
+ x_pos + self.base_width/2,
690
+ plot_y + tick_height/2,
691
+ self
692
+ )
693
+ self.tick_lines.append(tick_line)
694
+
695
+ # Add position number aligned with the plot line - remove the +1
696
+ end_pos = str(self.start_pos + current_pos + bases_this_line) # Removed +1
697
+ pos_item = QGraphicsSimpleTextItem(end_pos, self)
698
+ pos_item.setFont(QFont("Courier", 12))
699
+
700
+ # Calculate position for consistent alignment
701
+ text_width = pos_item.boundingRect().width()
702
+ pos_x = max_width + 10 # Fixed position based on maximum width
703
+ pos_y = plot_y - pos_item.boundingRect().height()/2
704
+ pos_item.setPos(pos_x, pos_y)
705
+
706
+ current_pos += bases_this_line
707
+
708
+ # Get parent view if it exists
709
+ view = self.scene().views()[0] if self.scene() and self.scene().views() else None
710
+ if view:
711
+ view.setUpdatesEnabled(True) # Re-enable updates if we have a view
712
+ self.update()
713
+
714
+ except Exception as e:
715
+ self.logger.error(f"Error in _create_display: {str(e)}")
716
+
717
+ def _reapply_highlights(self):
718
+ """Reapply stored highlights after recreating nucleotides"""
719
+ try:
720
+ # Clear existing highlights from nucleotides
721
+ for nuc in self.nucleotides:
722
+ nuc.is_highlighted = False
723
+ nuc.highlight_color = None
724
+
725
+ # Reapply each stored highlight
726
+ for start_pos, end_pos, color, strand in self.highlighted_regions:
727
+ # Calculate which lines contain the sequence
728
+ start_line = start_pos // self.bases_per_line
729
+ end_line = end_pos // self.bases_per_line
730
+
731
+ # Calculate positions within lines
732
+ start_pos_in_line = start_pos % self.bases_per_line
733
+ end_pos_in_line = end_pos % self.bases_per_line
734
+
735
+ # Get the correct strand's nucleotide map
736
+ strand_map = self.nucleotide_map[strand]
737
+
738
+ # Handle multi-line sequences
739
+ for line_num in range(start_line, end_line + 1):
740
+ if line_num >= len(strand_map):
741
+ continue
742
+
743
+ # Calculate start and end positions for this line
744
+ if line_num == start_line:
745
+ line_start = start_pos_in_line
746
+ else:
747
+ line_start = 0
748
+
749
+ if line_num == end_line:
750
+ line_end = end_pos_in_line
751
+ else:
752
+ line_end = self.bases_per_line - 1
753
+
754
+ # Get nucleotides for this line segment
755
+ line_nucleotides = strand_map[line_num]
756
+
757
+ # Calculate the range of nucleotides to highlight
758
+ start_idx = min(line_start, len(line_nucleotides))
759
+ end_idx = min(line_end + 1, len(line_nucleotides))
760
+
761
+ # Highlight the nucleotides
762
+ for i in range(start_idx, end_idx):
763
+ nuc = line_nucleotides[i]
764
+ nuc.is_highlighted = True
765
+ nuc.highlight_color = color
766
+ nuc.update()
767
+
768
+ except Exception as e:
769
+ self.logger.error(f"Error in _reapply_highlights: {str(e)}")
770
+
771
+ def _update_selection(self):
772
+ """Update the visual selection"""
773
+ try:
774
+ if self.selection_start is None:
775
+ self.logger.debug("No selection start point")
776
+ return
777
+
778
+ start_idx = min(self.selection_start, self.selection_end or self.selection_start)
779
+ end_idx = max(self.selection_start, self.selection_end or self.selection_start)
780
+
781
+ self.logger.debug(f"Updating selection: start={start_idx}, end={end_idx}")
782
+
783
+ # Update highlighting for all nucleotides
784
+ for i, nuc in enumerate(self.nucleotides):
785
+ if start_idx <= i <= end_idx:
786
+ nuc.is_highlighted = True
787
+ nuc.highlight_color = QColor(200, 200, 255, 100)
788
+ self.logger.debug(f"Highlighting nucleotide at position {i}: {nuc.nucleotide}")
789
+ else:
790
+ nuc.is_highlighted = False
791
+ nuc.highlight_color = None
792
+ nuc.update()
793
+
794
+ # Emit selection signal
795
+ if self.selection_active:
796
+ self.logger.debug(f"Emitting selection signal: {self.start_pos + start_idx} to {self.start_pos + end_idx}")
797
+ self.sequence_selected.emit(
798
+ self.start_pos + start_idx,
799
+ self.start_pos + end_idx
800
+ )
801
+
802
+ except Exception as e:
803
+ self.logger.error(f"Error in _update_selection: {str(e)}")
804
+
805
+ def mousePressEvent(self, event):
806
+ """Handle mouse press for selection start"""
807
+ try:
808
+ # Convert scene position to local coordinates
809
+ local_pos = self.mapFromScene(event.scenePos())
810
+
811
+ # Calculate base position more precisely
812
+ x_pos = local_pos.x()
813
+ line_number = int(local_pos.y() // (self.line_height * 2))
814
+
815
+ # Calculate which letter space was clicked
816
+ exact_position = x_pos / self.base_width
817
+ base_position = int(exact_position)
818
+
819
+ # Calculate if click was in left or right half of the letter space
820
+ fraction = exact_position - base_position
821
+ is_right_side = fraction > 0.5
822
+
823
+ # Adjust base_position based on where exactly the click occurred
824
+ if is_right_side:
825
+ cursor_index = base_position
826
+ cursor_side = 'right'
827
+ else:
828
+ cursor_index = max(0, base_position - 1)
829
+ cursor_side = 'right' if base_position == 0 else 'left'
830
+
831
+ # Calculate final index
832
+ index = line_number * self.bases_per_line + cursor_index
833
+ index = max(0, min(index, len(self.nucleotides) - 1))
834
+
835
+ if 0 <= index < len(self.nucleotides):
836
+ # Start selection
837
+ self.selection_active = True
838
+ self.selection_start = index
839
+ self.selection_end = index
840
+
841
+ # Update cursor position
842
+ nuc = self.nucleotides[index]
843
+ nuc.show_cursor = True
844
+ nuc.cursor_side = cursor_side
845
+
846
+ # Clear other cursors
847
+ for i, other_nuc in enumerate(self.nucleotides):
848
+ if i != index:
849
+ other_nuc.show_cursor = False
850
+ other_nuc.update()
851
+
852
+ # Update selection and cursor
853
+ cursor_pos = index + 1 if cursor_side == 'right' else index
854
+ self.cursor_position_changed.emit(self.start_pos + cursor_pos)
855
+ self._update_selection()
856
+ nuc.update()
857
+
858
+ except Exception as e:
859
+ print(f"Error in mousePressEvent: {str(e)}")
860
+
861
+ event.accept()
862
+
863
+ def mouseMoveEvent(self, event):
864
+ """Handle mouse move for selection update"""
865
+ if not self.selection_active:
866
+ return
867
+
868
+ try:
869
+ # Convert scene position to local coordinates
870
+ local_pos = self.mapFromScene(event.scenePos())
871
+
872
+ # Calculate base position more precisely
873
+ x_pos = local_pos.x()
874
+ line_number = int(local_pos.y() // (self.line_height * 2))
875
+
876
+ # Calculate which letter space was clicked
877
+ exact_position = x_pos / self.base_width
878
+ base_position = int(exact_position)
879
+
880
+ # Calculate if mouse is in left or right half of the letter space
881
+ fraction = exact_position - base_position
882
+ is_right_side = fraction > 0.5
883
+
884
+ # Adjust base_position based on mouse position
885
+ if is_right_side:
886
+ cursor_index = base_position
887
+ cursor_side = 'right'
888
+ else:
889
+ cursor_index = max(0, base_position - 1)
890
+ cursor_side = 'right' if base_position == 0 else 'left'
891
+
892
+ # Calculate final index
893
+ index = line_number * self.bases_per_line + cursor_index
894
+ index = max(0, min(index, len(self.nucleotides) - 1))
895
+
896
+ if 0 <= index < len(self.nucleotides):
897
+ # Update selection end point
898
+ self.selection_end = index
899
+
900
+ # Update cursor position
901
+ nuc = self.nucleotides[index]
902
+ nuc.show_cursor = True
903
+ nuc.cursor_side = cursor_side
904
+
905
+ # Clear other cursors
906
+ for i, other_nuc in enumerate(self.nucleotides):
907
+ if i != index:
908
+ other_nuc.show_cursor = False
909
+ other_nuc.update()
910
+
911
+ # Update selection and cursor
912
+ cursor_pos = index + 1 if cursor_side == 'right' else index
913
+ self.cursor_position_changed.emit(self.start_pos + cursor_pos)
914
+ self._update_selection()
915
+ nuc.update()
916
+
917
+ except Exception as e:
918
+ print(f"Error in mouseMoveEvent: {str(e)}")
919
+
920
+ event.accept()
921
+
922
+ def mouseReleaseEvent(self, event):
923
+ """Handle mouse release"""
924
+ if self.selection_active:
925
+ if self.selection_start is not None and self.selection_end is not None:
926
+ start_pos = min(self.selection_start, self.selection_end)
927
+ end_pos = max(self.selection_start, self.selection_end)
928
+
929
+ # Get selected sequence
930
+ selected_sequence = self._get_selected_sequence(start_pos, end_pos)
931
+
932
+ # Copy to clipboard
933
+ self.clipboard.setText(selected_sequence)
934
+
935
+ # Emit selection signal
936
+ self.sequence_selected.emit(
937
+ self.start_pos + start_pos,
938
+ self.start_pos + end_pos
939
+ )
940
+ else:
941
+ # If no selection was made, keep showing the insertion point
942
+ if hasattr(self, 'drag_start_pos') and self.drag_start_pos is not None:
943
+ self.cursor_position_changed.emit(self.start_pos + self.drag_start_pos)
944
+
945
+ event.accept()
946
+
947
+ def _find_closest_nucleotide(self, pos):
948
+ """Find the closest nucleotide to the given position"""
949
+ closest_item = None
950
+ min_distance = float('inf')
951
+
952
+ for nuc in self.nucleotides:
953
+ nuc_pos = nuc.scenePos()
954
+ distance = (pos.x() - nuc_pos.x()) ** 2 + (pos.y() - nuc_pos.y()) ** 2
955
+
956
+ if distance < min_distance:
957
+ min_distance = distance
958
+ closest_item = nuc
959
+
960
+ return closest_item
961
+
962
+ def _get_selected_sequence(self, start_pos, end_pos):
963
+ """Get the sequence of selected nucleotides"""
964
+ selected_nucs = []
965
+ for i in range(start_pos, end_pos + 1):
966
+ if i < len(self.nucleotides):
967
+ selected_nucs.append(self.nucleotides[i].nucleotide)
968
+ return ''.join(selected_nucs)
969
+
970
+ def keyPressEvent(self, event):
971
+ """Handle keyboard shortcuts"""
972
+ if event.matches(QKeySequence.StandardKey.Copy):
973
+ if self.selection_start is not None and self.selection_end is not None:
974
+ start_pos = min(self.selection_start, self.selection_end)
975
+ end_pos = max(self.selection_start, self.selection_end)
976
+ selected_sequence = self._get_selected_sequence(start_pos, end_pos)
977
+ self.clipboard.setText(selected_sequence)
978
+ super().keyPressEvent(event)
979
+
980
+ def highlight_sequence(self, start_pos, end_pos, color, strand='+'):
981
+ """Highlight sequence with proper strand handling"""
982
+ try:
983
+ # Store highlight information
984
+ self.highlighted_regions.append((start_pos, end_pos, color, strand))
985
+
986
+ # Calculate which lines contain the sequence
987
+ start_line = start_pos // self.bases_per_line
988
+ end_line = end_pos // self.bases_per_line
989
+
990
+ # Calculate positions within lines
991
+ start_pos_in_line = start_pos % self.bases_per_line
992
+ end_pos_in_line = end_pos % self.bases_per_line
993
+
994
+ # Get the correct strand's nucleotide map
995
+ strand_map = self.nucleotide_map[strand]
996
+
997
+ # Handle multi-line sequences
998
+ for line_num in range(start_line, end_line + 1):
999
+ if line_num >= len(strand_map):
1000
+ continue
1001
+
1002
+ # Calculate start and end positions for this line
1003
+ if line_num == start_line:
1004
+ line_start = start_pos_in_line
1005
+ else:
1006
+ line_start = 0
1007
+
1008
+ if line_num == end_line:
1009
+ line_end = end_pos_in_line
1010
+ else:
1011
+ line_end = self.bases_per_line - 1
1012
+
1013
+ # Get nucleotides for this line segment
1014
+ line_nucleotides = strand_map[line_num]
1015
+
1016
+ # Calculate the range of nucleotides to highlight
1017
+ start_idx = min(line_start, len(line_nucleotides))
1018
+ end_idx = min(line_end + 1, len(line_nucleotides))
1019
+
1020
+ # Highlight the nucleotides
1021
+ for i in range(start_idx, end_idx):
1022
+ nuc = line_nucleotides[i]
1023
+ nuc.is_highlighted = True
1024
+ nuc.highlight_color = color
1025
+ nuc.update()
1026
+
1027
+ self.logger.debug(
1028
+ f"Highlighted nucleotides on line {line_num} "
1029
+ f"for strand {strand} from {start_idx} to {end_idx}"
1030
+ )
1031
+
1032
+ except Exception as e:
1033
+ self.logger.error(f"Error in highlight_sequence: {str(e)}")
1034
+ self.logger.error(f"Start pos: {start_pos}, End pos: {end_pos}, Strand: {strand}")
1035
+
1036
+ def clear_highlights(self):
1037
+ """Clear all highlights"""
1038
+ self.highlighted_regions.clear()
1039
+ for nuc in self.nucleotides:
1040
+ nuc.is_highlighted = False
1041
+ nuc.highlight_color = None
1042
+ nuc.update()
1043
+
1044
+ def boundingRect(self):
1045
+ if not self.sequence:
1046
+ return QRectF()
1047
+
1048
+ # Calculate exact width based on actual sequence in last line
1049
+ last_line_length = len(self.sequence) % self.bases_per_line
1050
+ if last_line_length == 0 and len(self.sequence) > 0:
1051
+ last_line_length = self.bases_per_line
1052
+
1053
+ width = self.base_width * last_line_length + 100 # Add space for position numbers
1054
+
1055
+ # Calculate height using line spacing
1056
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
1057
+ height = total_lines * self.line_spacing
1058
+
1059
+ return QRectF(0, 0, width, height)
1060
+
1061
+ def get_nucleotide_position(self, nucleotide):
1062
+ """Get the position of a nucleotide in the sequence"""
1063
+ try:
1064
+ idx = self.nucleotides.index(nucleotide)
1065
+ return self.start_pos + idx
1066
+ except ValueError:
1067
+ return -1
1068
+
1069
+ def cleanup_graphics(self):
1070
+ """Clean up all graphics items"""
1071
+ if hasattr(self, 'plot_lines'):
1072
+ for line in self.plot_lines:
1073
+ if line in self.scene().items():
1074
+ self.scene().removeItem(line)
1075
+ self.plot_lines.clear()
1076
+
1077
+ if hasattr(self, 'tick_lines'):
1078
+ for line in self.tick_lines:
1079
+ if line in self.scene().items():
1080
+ self.scene().removeItem(line)
1081
+ self.tick_lines.clear()
1082
+
1083
+ for nuc in self.nucleotides:
1084
+ if nuc in self.scene().items():
1085
+ self.scene().removeItem(nuc)
1086
+ self.nucleotides.clear()
1087
+
1088
+ for item in self.scene().items():
1089
+ if isinstance(item, QGraphicsSimpleTextItem):
1090
+ self.scene().removeItem(item)
1091
+
1092
+ class FeatureViewer(QGraphicsObject):
1093
+ cursor_position_changed = pyqtSignal(int) # Add signal for cursor position
1094
+
1095
+ def __init__(self, parent=None):
1096
+ super().__init__(parent)
1097
+ self.sequence = ""
1098
+ self.features = []
1099
+ self.start_pos = 0
1100
+ self.base_width = 15
1101
+ self.bases_per_line = 70
1102
+ self.feature_height = 20
1103
+ self.line_height = 25
1104
+ self.feature_spacing = 2 # Reduce spacing between features and strands
1105
+ self.setAcceptHoverEvents(True) # Enable hover events
1106
+
1107
+ def set_data(self, sequence, features, start_pos):
1108
+ """Updated to accept sequence parameter"""
1109
+ self.sequence = sequence
1110
+ self.features = sorted(features, key=lambda x: x['start'])
1111
+ self.start_pos = start_pos
1112
+ self.update()
1113
+
1114
+ def paint(self, painter, option, widget):
1115
+ if not self.features or not self.sequence:
1116
+ return
1117
+
1118
+ # Process each line of sequence
1119
+ current_pos = 0
1120
+ while current_pos < len(self.sequence):
1121
+ line_text = self.sequence[current_pos:current_pos + self.bases_per_line]
1122
+ line_num = current_pos // self.bases_per_line
1123
+
1124
+ # Calculate y position to be directly below negative strand
1125
+ y_pos = line_num * self.line_height * 2
1126
+ feature_y = y_pos + self.line_height * 2 # Position directly below negative strand
1127
+
1128
+ # Calculate sequence width for this line
1129
+ sequence_width = len(line_text) * self.base_width
1130
+
1131
+ # Draw features
1132
+ for feature in self.features:
1133
+ try:
1134
+ # Calculate relative positions within current line
1135
+ feature_start = feature['start'] - current_pos
1136
+ feature_end = feature['end'] - current_pos
1137
+
1138
+ # Skip if feature is not in current line
1139
+ if feature_end < 0 or feature_start >= self.bases_per_line:
1140
+ continue
1141
+
1142
+ # Clip to line boundaries
1143
+ feature_start = max(0, feature_start)
1144
+ feature_end = min(self.bases_per_line, feature_end)
1145
+
1146
+ # Calculate pixel positions
1147
+ x_start = feature_start * self.base_width
1148
+ x_end = feature_end * self.base_width
1149
+
1150
+ # Create rectangle for feature
1151
+ feature_rect = QRectF(
1152
+ x_start,
1153
+ feature_y,
1154
+ x_end - x_start,
1155
+ self.feature_height
1156
+ )
1157
+
1158
+ # Draw orange rectangle
1159
+ painter.setBrush(QColor(255, 140, 0))
1160
+ painter.setPen(Qt.PenStyle.NoPen)
1161
+ painter.drawRect(feature_rect)
1162
+
1163
+ # Draw label if enough space
1164
+ label = feature.get('name', 'HFL1')
1165
+ text_width = painter.fontMetrics().horizontalAdvance(label)
1166
+ if (x_end - x_start) > text_width:
1167
+ text_x = x_start + ((x_end - x_start) - text_width) / 2
1168
+ text_y = feature_y + self.feature_height/2 + 4
1169
+ painter.setPen(Qt.GlobalColor.white)
1170
+ painter.setFont(QFont("Arial", 8))
1171
+ painter.drawText(QPointF(text_x, text_y), label)
1172
+
1173
+ except Exception as e:
1174
+ if hasattr(self, 'logger'):
1175
+ self.logger.error(f"Error drawing feature: {str(e)}")
1176
+ continue
1177
+
1178
+ current_pos += self.bases_per_line
1179
+
1180
+ def boundingRect(self):
1181
+ if not self.sequence:
1182
+ return QRectF()
1183
+
1184
+ # Calculate exact width based on sequence length
1185
+ last_line_length = len(self.sequence) % self.bases_per_line
1186
+ if last_line_length == 0:
1187
+ last_line_length = self.bases_per_line
1188
+ width = max(self.base_width * self.bases_per_line,
1189
+ self.base_width * last_line_length) + 100
1190
+
1191
+ # Calculate height for actual sequence lines only
1192
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
1193
+ height = total_lines * self.line_height * 2
1194
+
1195
+ return QRectF(0, 0, width, height)
1196
+
1197
+ def mousePressEvent(self, event):
1198
+ """Handle mouse press to show insertion point"""
1199
+ if event.button() == Qt.MouseButton.LeftButton:
1200
+ # Calculate position based on click location
1201
+ local_pos = event.pos()
1202
+ line_number = int(local_pos.y() // (self.line_height * 2))
1203
+ base_position = int(local_pos.x() // self.base_width)
1204
+
1205
+ # Calculate absolute position
1206
+ position = self.start_pos + line_number * self.bases_per_line + base_position
1207
+
1208
+ # Emit cursor position
1209
+ self.cursor_position_changed.emit(position)
1210
+
1211
+ event.accept()
1212
+
1213
+ def mouseMoveEvent(self, event):
1214
+ """Handle mouse move to update insertion point"""
1215
+ if event.buttons() & Qt.MouseButton.LeftButton:
1216
+ # Calculate position based on mouse location
1217
+ local_pos = event.pos()
1218
+ line_number = int(local_pos.y() // (self.line_height * 2))
1219
+ base_position = int(local_pos.x() // self.base_width)
1220
+
1221
+ # Calculate absolute position
1222
+ position = self.start_pos + line_number * self.bases_per_line + base_position
1223
+
1224
+ # Emit cursor position
1225
+ self.cursor_position_changed.emit(position)
1226
+
1227
+ event.accept()
src/views/ExportSelectedgRNAsView.py CHANGED
@@ -1,7 +1,6 @@
1
  from typing import Optional
2
  from PyQt6.QtWidgets import QMainWindow
3
  from PyQt6 import uic, QtWidgets
4
- import os
5
 
6
  class ExportSelectedgRNAsView(QMainWindow):
7
  def __init__(self, global_settings):
@@ -14,6 +13,10 @@ class ExportSelectedgRNAsView(QMainWindow):
14
  try:
15
  uic.loadUi(self.settings.get_ui_dir_path() + '/export_selected_gRNAs.ui', self)
16
  self.setWindowTitle("Export Selected gRNAs")
 
 
 
 
17
  self._init_ui_components()
18
  except Exception as e:
19
  self.logger.error(f"Error initializing ExportSelectedgRNAsView: {str(e)}", exc_info=True)
@@ -43,6 +46,13 @@ class ExportSelectedgRNAsView(QMainWindow):
43
  return widget
44
 
45
  def show_dialog(self) -> None:
 
 
 
 
 
 
 
46
  self.show()
47
  self.activateWindow()
48
 
 
1
  from typing import Optional
2
  from PyQt6.QtWidgets import QMainWindow
3
  from PyQt6 import uic, QtWidgets
 
4
 
5
  class ExportSelectedgRNAsView(QMainWindow):
6
  def __init__(self, global_settings):
 
13
  try:
14
  uic.loadUi(self.settings.get_ui_dir_path() + '/export_selected_gRNAs.ui', self)
15
  self.setWindowTitle("Export Selected gRNAs")
16
+
17
+ # Set fixed size for the window
18
+ self.setFixedSize(500, 300) # Width: 500px, Height: 300px
19
+
20
  self._init_ui_components()
21
  except Exception as e:
22
  self.logger.error(f"Error initializing ExportSelectedgRNAsView: {str(e)}", exc_info=True)
 
46
  return widget
47
 
48
  def show_dialog(self) -> None:
49
+ # Center the window on screen
50
+ screen = self.screen().availableGeometry()
51
+ self.move(
52
+ screen.center().x() - self.width() // 2,
53
+ screen.center().y() - self.height() // 2
54
+ )
55
+
56
  self.show()
57
  self.activateWindow()
58
 
src/views/FindTargetsView.py CHANGED
@@ -9,6 +9,7 @@ class FindTargetsView(QtWidgets.QMainWindow):
9
  def __init__(self, global_settings):
10
  super().__init__()
11
  self.global_settings = global_settings
 
12
  self._init_ui()
13
  self.batch_size = 100 # Number of rows to load at once
14
  self._all_results = [] # Store all results
@@ -67,31 +68,31 @@ class FindTargetsView(QtWidgets.QMainWindow):
67
  ]
68
 
69
  def display_results(self, results):
70
- start_time = time.time()
71
-
72
- # Store all results and reset loaded count
73
- self._all_results = results
74
- self._loaded_rows = 0
75
-
76
- # Disable visual updates
77
- self.results_table.setUpdatesEnabled(False)
78
- self.results_table.setSortingEnabled(False)
79
- self.results_table.setVisible(False)
80
-
81
- # Set total row count
82
- total_rows = len(results)
83
- self.results_table.setRowCount(total_rows)
84
-
85
- # Load initial batch
86
- self._load_batch(0, min(self.batch_size, total_rows))
87
-
88
- # Re-enable table and updates
89
- self.results_table.setVisible(True)
90
- self.results_table.setUpdatesEnabled(True)
91
- self.results_table.setSortingEnabled(True)
92
-
93
- total_time = time.time() - start_time
94
- self.global_settings.logger.debug(f"Initial display time: {total_time:.2f} seconds")
95
 
96
  def _load_batch(self, start_idx, end_idx):
97
  """Load a batch of rows efficiently"""
@@ -120,7 +121,6 @@ class FindTargetsView(QtWidgets.QMainWindow):
120
 
121
  # Calculate which rows should be visible
122
  scroll_position = value
123
- start_row = max(0, scroll_position - visible_rows)
124
  end_row = min(len(self._all_results), scroll_position + visible_rows * 2)
125
 
126
  # Load more rows if needed
@@ -128,23 +128,46 @@ class FindTargetsView(QtWidgets.QMainWindow):
128
  self._load_batch(self._loaded_rows, end_row)
129
 
130
  def get_selected_targets(self):
131
- selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
132
- selected_targets = []
133
-
134
- for row in selected_rows:
135
- if row < len(self._all_results):
136
- selected_targets.append(self._all_results[row])
137
-
138
- return selected_targets
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
 
 
 
 
 
 
 
140
  def clear_results(self):
141
- """Clear all results from the table"""
142
- self.results_table.setUpdatesEnabled(False)
143
  self.results_table.clearContents()
144
- self.results_table.setRowCount(0)
145
- self._all_results = []
146
- self._loaded_rows = 0
147
- self.results_table.setUpdatesEnabled(True)
148
 
149
  def _on_generate_library_clicked(self):
150
  """Handle generate library button click"""
 
9
  def __init__(self, global_settings):
10
  super().__init__()
11
  self.global_settings = global_settings
12
+ self.logger = global_settings.logger
13
  self._init_ui()
14
  self.batch_size = 100 # Number of rows to load at once
15
  self._all_results = [] # Store all results
 
68
  ]
69
 
70
  def display_results(self, results):
71
+ """Display results with filtering support"""
72
+ try:
73
+ # Store all results and reset loaded count
74
+ self._all_results = results
75
+ self._loaded_rows = 0
76
+
77
+ # Disable visual updates
78
+ self.results_table.setUpdatesEnabled(False)
79
+ self.results_table.setSortingEnabled(False)
80
+ self.results_table.setVisible(False)
81
+
82
+ # Set total row count
83
+ total_rows = len(results)
84
+ self.results_table.setRowCount(total_rows)
85
+
86
+ # Load initial batch
87
+ self._load_batch(0, min(self.batch_size, total_rows))
88
+
89
+ # Re-enable table and updates
90
+ self.results_table.setVisible(True)
91
+ self.results_table.setUpdatesEnabled(True)
92
+ self.results_table.setSortingEnabled(True)
93
+
94
+ except Exception as e:
95
+ self.logger.error(f"Error displaying results: {str(e)}")
96
 
97
  def _load_batch(self, start_idx, end_idx):
98
  """Load a batch of rows efficiently"""
 
121
 
122
  # Calculate which rows should be visible
123
  scroll_position = value
 
124
  end_row = min(len(self._all_results), scroll_position + visible_rows * 2)
125
 
126
  # Load more rows if needed
 
128
  self._load_batch(self._loaded_rows, end_row)
129
 
130
  def get_selected_targets(self):
131
+ """Get selected targets from the currently displayed (filtered) results"""
132
+ try:
133
+ # Get indices of selected rows in the current view
134
+ selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
135
+ selected_targets = []
136
+
137
+ # Get the currently visible rows from the table
138
+ visible_targets = []
139
+ for row in range(self.results_table.rowCount()):
140
+ if not self.results_table.isRowHidden(row):
141
+ # Get data from visible row
142
+ target_data = {
143
+ 'feature_type': self.results_table.item(row, 0).text(),
144
+ 'chromosome': self.results_table.item(row, 1).text(),
145
+ 'feature_id': self.results_table.item(row, 2).text(),
146
+ 'feature_name': self.results_table.item(row, 3).text(),
147
+ 'feature_description': self.results_table.item(row, 4).text()
148
+ }
149
+ visible_targets.append((row, target_data))
150
+
151
+ # Match selected rows with visible targets
152
+ for row, target_data in visible_targets:
153
+ if row in selected_rows:
154
+ # Find corresponding full target data from _all_results
155
+ for full_target in self._all_results:
156
+ if (full_target['feature_id'] == target_data['feature_id'] and
157
+ full_target['feature_type'] == target_data['feature_type']):
158
+ selected_targets.append(full_target)
159
+ break
160
 
161
+ self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
162
+ return selected_targets
163
+
164
+ except Exception as e:
165
+ self.logger.error(f"Error getting selected targets: {str(e)}")
166
+ return []
167
+
168
  def clear_results(self):
 
 
169
  self.results_table.clearContents()
170
+ self.results_table.setRowCount(0)
 
 
 
171
 
172
  def _on_generate_library_clicked(self):
173
  """Handle generate library button click"""
src/views/GenBankParse.py DELETED
@@ -1,82 +0,0 @@
1
- __author__ = 'brianmendoza'
2
-
3
- from Bio import Entrez, SeqIO
4
- import webbrowser
5
- import re
6
- import os
7
-
8
-
9
- class GenBankFile:
10
-
11
- def __init__(self, organism):
12
- Entrez.email = "bmendoz1@vols.utk.edu"
13
- self.directory = "/Users/brianmendoza/Desktop/GenBank_files/"
14
- self.org = organism
15
-
16
- def setOrg(self, org):
17
- self.org = org
18
-
19
- def setDirectory(self, path, org):
20
- self.directory = path
21
- self.setOrg(org)
22
-
23
- def convertToFasta(self):
24
- orgfile = self.directory + self.org + ".gbff"
25
- output = "/Users/brianmendoza/Desktop/GenBank_files/FASTAs/" + self.org + ".fna"
26
- SeqIO.convert(orgfile, "genbank", output, "fasta")
27
-
28
- def parseAnnotation(self):
29
- gb_file = self.directory + self.org + ".gbff"
30
- records = SeqIO.parse(open(gb_file,"r"), "genbank")
31
-
32
- # create table for multi-targeting reference
33
- table = {}
34
- count = 0
35
- for record in records:
36
- count += 1
37
- chrmnumber = str(count)
38
- table[chrmnumber] = []
39
- for feature in record.features:
40
- if feature.type == 'CDS': # hopefully gene and CDS are the same
41
-
42
- # getting the location...
43
- loc = str(feature.location)
44
- out = re.findall(r"[\d]+", loc)
45
- start = out[0]
46
- end = out[1]
47
- if len(out) > 2: # to account for "joined" domains
48
- end = out[3]
49
-
50
- # locus_tag and product...
51
- if 'locus_tag' in feature.qualifiers:
52
- ltag = feature.qualifiers['locus_tag']
53
- elif 'gene' in feature.qualifiers:
54
- ltag = feature.qualifiers['gene']
55
- if 'product' not in feature.qualifiers:
56
- prod = feature.qualifiers['note']
57
- else:
58
- prod = feature.qualifiers['product']
59
- # adding it all up...
60
- tup = (start, end, ltag, prod)
61
- table[chrmnumber].append(tup)
62
- return table
63
-
64
- def getChromSequence(self, index):
65
- gb_file = self.directory + self.org + ".gbff"
66
- records = SeqIO.parse(open(gb_file,"r"), "genbank")
67
- count = 0
68
- for record in records:
69
- count += 1
70
- if count == index:
71
- cstr = record.seq
72
- return cstr
73
-
74
-
75
- class GffFile:
76
-
77
- def __init__(self, organism):
78
- self.directory = "/Users/brianmendoza/Desktop/GenBank_Files/"
79
- self.org = organism
80
-
81
-
82
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/views/HomeWindowView.py CHANGED
@@ -103,10 +103,11 @@ class HomeWindowView(QWidget):
103
  # self.combo_box_local_annotation_files.addItems(annotation_files)
104
 
105
  def get_find_targets_input(self) -> dict:
 
106
  return {
107
  "organism": self.combo_box_organism.currentText(),
108
  "endonuclease": self.combo_box_endonuclease.currentText(),
109
- "annotation_file": self.combo_box_local_annotation_files.currentText(),
110
  "search_type": self.get_search_type(),
111
  "search_query": self.text_edit_gene_entry.toPlainText()
112
  }
@@ -130,13 +131,19 @@ class HomeWindowView(QWidget):
130
  # Clear existing items
131
  self.combo_box_local_annotation_files.clear()
132
 
133
- # Filter out .index files
134
- filtered_files = [f for f in files if not f.endswith('.index')]
 
 
 
135
 
136
  # Add filtered files to combo box
137
  if filtered_files:
138
  self.combo_box_local_annotation_files.addItems(filtered_files)
 
139
  self.combo_box_local_annotation_files.setCurrentIndex(0)
 
 
140
  self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
141
  else:
142
  self.logger.debug("No local annotation files found")
@@ -144,6 +151,18 @@ class HomeWindowView(QWidget):
144
  except Exception as e:
145
  self.logger.error(f"Error updating local annotation files: {str(e)}")
146
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def show_warning(self, title: str, message: str) -> None:
148
  """Show a warning message dialog"""
149
  QtWidgets.QMessageBox.warning(self, title, message)
 
103
  # self.combo_box_local_annotation_files.addItems(annotation_files)
104
 
105
  def get_find_targets_input(self) -> dict:
106
+ current_annotation = self.combo_box_local_annotation_files.currentText()
107
  return {
108
  "organism": self.combo_box_organism.currentText(),
109
  "endonuclease": self.combo_box_endonuclease.currentText(),
110
+ "annotation_file": current_annotation,
111
  "search_type": self.get_search_type(),
112
  "search_query": self.text_edit_gene_entry.toPlainText()
113
  }
 
131
  # Clear existing items
132
  self.combo_box_local_annotation_files.clear()
133
 
134
+ # Filter out .index files and ensure files are valid
135
+ filtered_files = [
136
+ f for f in files
137
+ if not f.endswith('.index') and f.strip()
138
+ ]
139
 
140
  # Add filtered files to combo box
141
  if filtered_files:
142
  self.combo_box_local_annotation_files.addItems(filtered_files)
143
+ # Set the first item as current
144
  self.combo_box_local_annotation_files.setCurrentIndex(0)
145
+ # Emit the change signal to update the current annotation file
146
+ self._on_annotation_file_changed(self.combo_box_local_annotation_files.currentText())
147
  self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
148
  else:
149
  self.logger.debug("No local annotation files found")
 
151
  except Exception as e:
152
  self.logger.error(f"Error updating local annotation files: {str(e)}")
153
 
154
+ def _on_annotation_file_changed(self, new_file):
155
+ """Handle annotation file changes"""
156
+ try:
157
+ if new_file:
158
+ self.logger.debug(f"Setting current annotation file to: {new_file}")
159
+ self.global_settings.set_current_annotation_file(new_file)
160
+ # Ensure the combo box reflects the current selection
161
+ if self.combo_box_local_annotation_files.currentText() != new_file:
162
+ self.combo_box_local_annotation_files.setCurrentText(new_file)
163
+ except Exception as e:
164
+ self.logger.error(f"Error handling annotation file change: {str(e)}")
165
+
166
  def show_warning(self, title: str, message: str) -> None:
167
  """Show a warning message dialog"""
168
  QtWidgets.QMessageBox.warning(self, title, message)
src/views/LoadingDialog.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QDialog, QProgressBar, QVBoxLayout, QLabel
2
+ from PyQt6.QtCore import Qt
3
+ from PyQt6.QtWidgets import QApplication
4
+
5
+ class LoadingDialog(QDialog):
6
+ def __init__(self, parent=None, message="Loading..."):
7
+ super().__init__(parent)
8
+ self.setWindowTitle("Please Wait")
9
+ self.setWindowModality(Qt.WindowModality.ApplicationModal)
10
+ self.setFixedSize(300, 100)
11
+
12
+ # Remove window decorations and set dialog flags
13
+ self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
14
+
15
+ # Create layout
16
+ layout = QVBoxLayout()
17
+
18
+ # Add message label
19
+ self.label = QLabel(message)
20
+ self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
21
+ layout.addWidget(self.label)
22
+
23
+ self.progress_bar = QProgressBar()
24
+ self.progress_bar.setRange(0, 100) # Set range for percentage
25
+ layout.addWidget(self.progress_bar)
26
+
27
+ self.setLayout(layout)
28
+
29
+ # Center on main window
30
+ self.center_on_parent()
31
+
32
+ def center_on_parent(self):
33
+ """Center the dialog on the main window or parent"""
34
+ parent = self.parent()
35
+ if parent:
36
+ # Get the main window from global settings if available
37
+ main_window = None
38
+ if hasattr(parent, 'global_settings'):
39
+ main_window = parent.global_settings.main_window
40
+ elif hasattr(parent, 'settings'):
41
+ main_window = parent.settings.main_window
42
+
43
+ # Get geometry of the window to center on
44
+ if main_window and main_window.view:
45
+ geometry = main_window.view.geometry()
46
+ else:
47
+ geometry = parent.geometry()
48
+
49
+ # Calculate center position
50
+ x = geometry.x() + (geometry.width() - self.width()) // 2
51
+ y = geometry.y() + (geometry.height() - self.height()) // 2
52
+
53
+ # Ensure dialog stays within screen bounds
54
+ screen = QApplication.primaryScreen().geometry()
55
+ x = max(screen.left(), min(x, screen.right() - self.width()))
56
+ y = max(screen.top(), min(y, screen.bottom() - self.height()))
57
+
58
+ self.move(x, y)
59
+
60
+ def set_message(self, message, progress=None):
61
+ """Update the loading message and optionally the progress"""
62
+ if progress is not None:
63
+ self.progress_bar.setValue(progress)
64
+ self.label.setText(message)
65
+
66
+ # Recenter after updating message
67
+ self.center_on_parent()
68
+
69
+ def set_progress(self, value):
70
+ """Set progress value (0-100)"""
71
+ self.progress_bar.setValue(value)
72
+ self.label.setText("Loading...")
73
+
74
+ def set_indeterminate(self):
75
+ """Set indeterminate progress"""
76
+ self.progress_bar.setRange(0, 0)
77
+ self.label.setText("Loading...")
78
+
79
+ def showEvent(self, event):
80
+ """Override show event to ensure dialog is centered when shown"""
81
+ super().showEvent(event)
82
+ self.center_on_parent()
83
+
src/views/MainWindowUI.py DELETED
@@ -1,152 +0,0 @@
1
- from PyQt6 import QtWidgets, QtGui, QtCore, uic
2
- import os
3
-
4
- class MainWindowUI(QtWidgets.QMainWindow):
5
- def __init__(self, settings):
6
- super(MainWindowUI, self).__init__()
7
- self.settings = settings
8
- self.setup_ui()
9
-
10
- def setup_ui(self):
11
- # Load the UI file
12
- uic.loadUi(os.path.join(self.settings.get_ui_dir(), 'main_window.ui'), self)
13
-
14
- # Set window properties
15
- self.setWindowTitle("CASPER")
16
- self.setWindowIcon(QtGui.QIcon(os.path.join(self.settings.get_assets_dir(), "cas9image.ico")))
17
-
18
- # Initialize UI components
19
- self.init_ui_components()
20
-
21
- # Set up styles
22
- self.set_styles()
23
-
24
- # Initialize progress bar to 0
25
- self.progressBar.setValue(0)
26
-
27
- # Add the theme toggle button
28
- self.theme_toggle_button = QtWidgets.QPushButton(self)
29
- self.theme_toggle_button.setFixedSize(32, 32)
30
- self.theme_toggle_button.setStyleSheet("border: none;")
31
- self.update_theme_icon()
32
-
33
- # Position the button in the top right corner
34
- self.theme_toggle_button.setGeometry(self.width() - 40, 10, 32, 32)
35
-
36
- # Connect the button to a slot (to be implemented in the controller)
37
- self.theme_toggle_button.clicked.connect(self.on_theme_toggle)
38
-
39
- def init_ui_components(self):
40
- # Initialize and find all the UI components
41
- self.org_choice = self.findChild(QtWidgets.QComboBox, 'orgChoice')
42
- self.endo_choice = self.findChild(QtWidgets.QComboBox, 'endoChoice')
43
- self.annotation_files = self.findChild(QtWidgets.QComboBox, 'annotation_files')
44
- self.gene_entry_field = self.findChild(QtWidgets.QPlainTextEdit, 'gene_entry_field')
45
-
46
- self.push_button_find_targets = self.findChild(QtWidgets.QPushButton, 'pushButton_FindTargets')
47
- self.push_button_view_targets = self.findChild(QtWidgets.QPushButton, 'pushButton_ViewTargets')
48
- self.generate_library = self.findChild(QtWidgets.QPushButton, 'GenerateLibrary')
49
-
50
- self.radio_button_gene = self.findChild(QtWidgets.QRadioButton, 'radioButton_Gene')
51
- self.radio_button_position = self.findChild(QtWidgets.QRadioButton, 'radioButton_Position')
52
- self.radio_button_sequence = self.findChild(QtWidgets.QRadioButton, 'radioButton_Sequence')
53
-
54
- self.new_genome_button = self.findChild(QtWidgets.QPushButton, 'newGenome_button')
55
- self.new_endo_button = self.findChild(QtWidgets.QPushButton, 'newEndo_button')
56
- self.multitargeting_button = self.findChild(QtWidgets.QPushButton, 'multitargeting_button')
57
- self.population_analysis_button = self.findChild(QtWidgets.QPushButton, 'populationAnalysis_button')
58
- self.combine_files_button = self.findChild(QtWidgets.QPushButton, 'combineFiles_button')
59
-
60
- self.progress_bar = self.findChild(QtWidgets.QProgressBar, 'progressBar')
61
-
62
- self.step1 = self.findChild(QtWidgets.QGroupBox, 'Step1')
63
- self.step2 = self.findChild(QtWidgets.QGroupBox, 'Step2')
64
- self.step3 = self.findChild(QtWidgets.QGroupBox, 'Step3')
65
- self.casper_navigation = self.findChild(QtWidgets.QGroupBox, 'CASPER_Navigation')
66
-
67
- self.ncbi_button = self.findChild(QtWidgets.QPushButton, 'ncbi_button')
68
-
69
- # Connect the actionChange_Directory to a slot
70
- self.actionChange_Directory.triggered.connect(self.on_change_directory)
71
-
72
- def set_styles(self):
73
- groupbox_style = """
74
- QGroupBox:title{subcontrol-origin: margin;
75
- left: 10px;
76
- padding: 0 5px 0 5px;}
77
- QGroupBox#Step1{border: 2px solid rgb(111,181,110);
78
- border-radius: 9px;
79
- margin-top: 10px;
80
- font: bold 14pt 'Arial';}
81
- """
82
- self.step1.setStyleSheet(groupbox_style)
83
- self.step2.setStyleSheet(groupbox_style.replace("Step1", "Step2"))
84
- self.step3.setStyleSheet(groupbox_style.replace("Step1", "Step3"))
85
- self.casper_navigation.setStyleSheet(groupbox_style.replace("Step1", "CASPER_Navigation")
86
- .replace("solid","dashed")
87
- .replace("rgb(111,181,110)","rgb(88,89,91)"))
88
-
89
- def set_gene_entry_placeholder(self):
90
- placeholder_text = ("Example Inputs: \n\n"
91
- "Option 1: Feature (ID, Locus Tag, or Name)\n"
92
- "Example: 854068/YOL086C/ADH1 for S. cerevisiae alcohol dehydrogenase 1\n\n"
93
- "Option 2: Position (chromosome,start,stop)\n"
94
- "Example: 1,1,1000 for targeting chromosome 1, base pairs 1 to 1000\n\n"
95
- "Option 3: Sequence (must be within the selected organism)\n"
96
- "Example: Any nucleotide sequence between 100 and 10,000 base pairs.\n\n"
97
- "*Note: to multiplex, separate multiple queries by new lines*\n"
98
- "Example:\n"
99
- "1,1,1000\n"
100
- "5,1,500\n"
101
- "etc.")
102
- self.gene_entry_field.setPlaceholderText(placeholder_text)
103
-
104
- def enable_view_targets(self, enable):
105
- self.push_button_view_targets.setEnabled(enable)
106
-
107
- def enable_generate_library(self, enable):
108
- self.generate_library.setEnabled(enable)
109
-
110
- def set_progress(self, value):
111
- if self.progressBar:
112
- self.progressBar.setValue(value)
113
-
114
- def reset_progress(self):
115
- if self.progressBar:
116
- self.progressBar.setValue(0)
117
-
118
- def toggle_annotation(self, gene_checked):
119
- self.step2.setEnabled(True)
120
-
121
- def update_endo_choice(self, endos):
122
- self.endo_choice.clear()
123
- self.endo_choice.addItems(endos)
124
-
125
- def bring_to_front(self):
126
- self.show()
127
- self.setWindowState(self.windowState() & ~QtCore.Qt.WindowState.WindowMinimized | QtCore.Qt.WindowState.WindowActive)
128
- self.raise_()
129
- self.activateWindow()
130
- QtWidgets.QApplication.setActiveWindow(self)
131
-
132
- def on_change_directory(self):
133
- # This method will be connected to the controller
134
- pass
135
-
136
- def update_theme_icon(self):
137
- # Swap the icons: use dark_mode.png for light mode, and light_mode.png for dark mode
138
- icon = QtGui.QIcon(os.path.join(self.settings.get_assets_dir(), "dark_mode.png" if self.settings.is_dark_mode() else "light_mode.png"))
139
- self.theme_toggle_button.setIcon(icon)
140
- self.theme_toggle_button.setIconSize(QtCore.QSize(24, 24))
141
-
142
- # Update the entire application's theme
143
- self.settings.apply_theme()
144
-
145
- def resizeEvent(self, event):
146
- super().resizeEvent(event)
147
- # Reposition the theme toggle button when the window is resized
148
- self.theme_toggle_button.setGeometry(self.width() - 40, 10, 32, 32)
149
-
150
- def on_theme_toggle(self):
151
- # This method will be connected to the controller
152
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/views/MainWindowView copy.py DELETED
@@ -1,265 +0,0 @@
1
- from PyQt6.QtWidgets import QMainWindow, QPushButton, QRadioButton, QComboBox, QPlainTextEdit, QProgressBar, QMenuBar, QMenu, QStackedWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame
2
- from PyQt6.QtGui import QIcon, QAction, QFont
3
- from PyQt6.QtCore import Qt, QPoint
4
- from PyQt6 import uic, QtWidgets, QtCore
5
- from utils.ui import scale_ui, show_error
6
- import os
7
- from typing import Optional
8
-
9
- class MainWindowView(QMainWindow):
10
- def __init__(self, global_settings):
11
- super().__init__()
12
- self.global_settings = global_settings
13
- self._init_ui()
14
-
15
- def _init_ui(self) -> None:
16
- try:
17
- # self._load_ui_file()
18
- self._init_window_properties()
19
- self._init_custom_title_bar()
20
- self._init_ui_elements()
21
- self._scale_ui()
22
- except Exception as e:
23
- self._handle_init_error(e)
24
-
25
- def _load_ui_file(self) -> None:
26
- ui_file = os.path.join(self.global_settings.get_ui_dir(), "home_window.ui")
27
- uic.loadUi(ui_file, self)
28
-
29
- def _init_window_properties(self) -> None:
30
- self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
31
- self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
32
-
33
- def _init_ui_elements(self) -> None:
34
- # Create a main widget to hold everything
35
- main_widget = QWidget()
36
- main_layout = QVBoxLayout(main_widget)
37
- main_layout.setContentsMargins(0, 0, 0, 0)
38
- main_layout.setSpacing(0)
39
-
40
- # Add the custom title bar
41
- main_layout.addWidget(self.title_bar)
42
-
43
- # Create and add the divider
44
- divider = QFrame()
45
- divider.setFrameShape(QFrame.Shape.HLine)
46
- divider.setFrameShadow(QFrame.Shadow.Sunken)
47
- divider.setStyleSheet("background-color: #c0c0c0;") # Light gray color
48
- divider.setFixedHeight(1) # 1 pixel height
49
- main_layout.addWidget(divider)
50
-
51
- # Create a widget to hold the original content
52
- content_widget = QWidget()
53
- content_layout = QVBoxLayout(content_widget)
54
-
55
- # Move the existing widgets to the content layout
56
- for child in self.children():
57
- if isinstance(child, (QMenuBar, QWidget)) and child != self.title_bar:
58
- content_layout.addWidget(child)
59
-
60
- # Create the stacked widget
61
- self.stacked_widget = QStackedWidget()
62
-
63
- # Add the content widget and stacked widget to the main layout
64
- main_layout.addWidget(content_widget)
65
- main_layout.addWidget(self.stacked_widget)
66
-
67
- # Set the main widget as the central widget
68
- self.setCentralWidget(main_widget)
69
-
70
- # Initialize other UI elements
71
- self._init_menuBar()
72
- self._init_grpNavigationMenu()
73
- self._init_grpStep1()
74
- self._init_grpStep2()
75
- self._init_grpStep3()
76
-
77
- def _init_custom_title_bar(self) -> None:
78
- self.title_bar = QWidget(self)
79
- self.title_bar.setObjectName("custom_title_bar")
80
- self.title_bar.setFixedHeight(32) # Reduced height
81
-
82
- # Create the main horizontal layout for the title bar
83
- layout = QHBoxLayout(self.title_bar)
84
- layout.setContentsMargins(10, 0, 10, 0) # Equal margins on left and right
85
- layout.setSpacing(5) # Reduced spacing between items
86
-
87
- # ----- Window Control Buttons -----
88
- button_font = QFont("Arial", 8) # Smaller font size for button text
89
-
90
- self.minimize_button = QPushButton("-", self.title_bar)
91
- self.minimize_button.setObjectName("minimize_button")
92
- self.minimize_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
93
- self.minimize_button.setFont(button_font)
94
-
95
- self.maximize_button = QPushButton("⛶", self.title_bar)
96
- self.maximize_button.setObjectName("maximize_button")
97
- self.maximize_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
98
- self.maximize_button.setFont(button_font)
99
-
100
- self.close_button = QPushButton("✕", self.title_bar)
101
- self.close_button.setObjectName("close_button")
102
- self.close_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
103
- self.close_button.setFont(button_font)
104
-
105
- # Apply a style to center the text vertically and horizontally
106
- button_style = """
107
- QPushButton {
108
- padding: 0px;
109
- margin: 0px;
110
- line-height: 20px;
111
- text-align: center;
112
- }
113
- """
114
- self.minimize_button.setStyleSheet(button_style)
115
- self.maximize_button.setStyleSheet(button_style)
116
- self.close_button.setStyleSheet(button_style)
117
-
118
- # ----- Left Widget (Minimize, Maximize, Close) -----
119
- left_widget = QWidget()
120
- left_layout = QHBoxLayout(left_widget)
121
- left_layout.setContentsMargins(0, 0, 0, 0)
122
- left_layout.setSpacing(5)
123
- left_layout.addWidget(self.close_button)
124
- left_layout.addWidget(self.minimize_button)
125
- left_layout.addWidget(self.maximize_button)
126
-
127
- # ----- Theme Toggle Button -----
128
- self.theme_toggle_button = QPushButton(self.title_bar)
129
- self.theme_toggle_button.setObjectName("theme_toggle_button")
130
- self.theme_toggle_button.setFixedSize(20, 20) # Reduced size from 24x24 to 20x20
131
- self.theme_toggle_button.setStyleSheet("border: none;")
132
- self.update_theme_icon()
133
-
134
- # ----- Right Widget (Theme Toggle + Stretch) -----
135
- right_widget = QWidget()
136
- right_layout = QHBoxLayout(right_widget)
137
- right_layout.setContentsMargins(0, 0, 0, 0)
138
- right_layout.setSpacing(5)
139
-
140
- # Add a stretch to push the toggle button to the far right within right_widget
141
- right_layout.addStretch()
142
- right_layout.addWidget(self.theme_toggle_button)
143
-
144
- # ----- Synchronize Widths of Left and Right Widgets -----
145
- # Adjust left_widget to calculate its required width
146
- left_widget.adjustSize()
147
- left_width = left_widget.sizeHint().width()
148
-
149
- # Set right_widget's fixed width to match left_widget's width
150
- right_widget.setFixedWidth(left_width)
151
-
152
- # ----- Title Label -----
153
- self.title_label = QLabel("CASPER", self.title_bar)
154
- self.title_label.setObjectName("title_label")
155
- self.title_label.setFont(QFont("Arial", 10, QFont.Weight.Bold)) # Reduced font size
156
- self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text in the label
157
- self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
158
-
159
- # ----- Add Widgets to the Main Title Bar Layout -----
160
- layout.addWidget(left_widget) # Left side buttons
161
- layout.addStretch(1) # Stretchable space
162
- layout.addWidget(self.title_label) # Centered title
163
- layout.addStretch(1) # Stretchable space
164
- layout.addWidget(right_widget) # Right side buttons (theme toggle + stretch)
165
-
166
- # Optional: Ensure the title_label is truly centered
167
- self.title_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
168
- def _init_menuBar(self) -> None:
169
- self.action_change_directory = self._find_widget("actChangeDirectory", QAction)
170
- self.action_open_genome_browser = self._find_widget("actOpenGenomeBrowser", QAction)
171
- self.action_open_repository = self._find_widget("actOpenRepository", QAction)
172
- self.action_open_NCBI_BLAST = self._find_widget("actOpenNCBIBLAST", QAction)
173
- self.action_open_NCBI = self._find_widget("actOpenNCBI", QAction)
174
-
175
- def _init_grpNavigationMenu(self) -> None:
176
- self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
177
- self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
178
- self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
179
- self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
180
- self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
181
-
182
- def _init_grpStep1(self) -> None:
183
- self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
184
- self.combo_box_endonuclease = self._find_widget("cmbEndonuclease", QComboBox)
185
-
186
- def _init_grpStep2(self) -> None:
187
- self.push_button_ncbi_file_search = self._find_widget("pbtnNCBIFileSearch", QPushButton)
188
- self.combo_box_local_annotation_files = self._find_widget("cmbLocalAnnotationFiles", QComboBox)
189
-
190
- def _init_grpStep3(self) -> None:
191
- self.radio_button_feature = self._find_widget("rbtnFeature", QRadioButton)
192
- self.radio_button_position = self._find_widget("rbtnPosition", QRadioButton)
193
- self.radio_button_sequence = self._find_widget("rbtnSequence", QRadioButton)
194
- self.text_edit_gene_entry = self._find_widget("txtedGeneEntry", QPlainTextEdit)
195
- self.push_button_find_targets = self._find_widget("pbtnFindTargets", QPushButton)
196
- self.progress_bar_find_targets = self._find_widget("progBarFindTargets", QProgressBar)
197
- self.push_button_view_targets = self._find_widget("pbtnViewTargets", QPushButton)
198
- self.push_button_generate_library = self._find_widget("pbtnGenerateLibrary", QPushButton)
199
-
200
- placeholder_text = ("Example Inputs: \n\n"
201
- "Option 1: Feature (ID, Locus Tag, or Name)\n"
202
- "Example: 854068/YOL086C/ADH1 for S. cerevisiae alcohol dehydrogenase 1\n\n"
203
- "Option 2: Position (chromosome,start,stop)\n"
204
- "Example: 1,1,1000 for targeting chromosome 1, base pairs 1 to 1000\n\n"
205
- "Option 3: Sequence (must be within the selected organism)\n"
206
- "Example: Any nucleotide sequence between 100 and 10,000 base pairs.\n\n"
207
- "*Note: to multiplex, separate multiple queries by new lines*\n"
208
- "Example:\n"
209
- "1,1,1000\n"
210
- "5,1,500\n"
211
- "etc.")
212
- self.text_edit_gene_entry.setPlaceholderText(placeholder_text)
213
-
214
- def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
215
- widget = self.findChild(widget_type, name)
216
- if widget is None:
217
- self.global_settings.logger.warning(f"Widget '{name}' not found in UI file.")
218
- return widget
219
-
220
- def _scale_ui(self) -> None:
221
- scale_ui(self, custom_scale_width=1000, custom_scale_height=350)
222
-
223
- def _handle_init_error(self, e: Exception) -> None:
224
- error_msg = f"Error initializing MainWindowView: {str(e)}"
225
- self.global_settings.logger.error(error_msg, exc_info=True)
226
- show_error(self.global_settings, "Initialization Error", error_msg)
227
- raise
228
-
229
- def update_combo_box_endonuclease(self, endonuclease: list) -> None:
230
- self.combo_box_endonuclease.clear()
231
- self.combo_box_endonuclease.addItems(endonuclease)
232
-
233
- def update_combo_box_organism(self, organisms: list) -> None:
234
- self.combo_box_organism.clear()
235
- self.combo_box_organism.addItems(organisms)
236
-
237
- def update_combo_box_annotation_files(self, annotation_files: list) -> None:
238
- self.combo_box_local_annotation_files.clear()
239
- self.combo_box_local_annotation_files.addItems(annotation_files)
240
-
241
- def set_progress_bar(self, value: int) -> None:
242
- self.progress_bar_find_targets.setValue(value)
243
-
244
- def reset_progress_bar(self) -> None:
245
- self.set_progress_bar(0)
246
-
247
- def update_theme_icon(self) -> None:
248
- icon_name = "dark_mode.png" if self.global_settings.is_dark_mode() else "light_mode.png"
249
- icon_path = os.path.join(self.global_settings.get_assets_dir(), icon_name)
250
- icon = QIcon(icon_path)
251
- self.theme_toggle_button.setIcon(icon)
252
- self.theme_toggle_button.setIconSize(QtCore.QSize(16, 16)) # Reduced icon size from 18x18 to 16x16
253
-
254
- def mousePressEvent(self, event):
255
- if event.button() == Qt.MouseButton.LeftButton and self.title_bar.underMouse():
256
- self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
257
- event.accept()
258
-
259
- def mouseMoveEvent(self, event):
260
- if event.buttons() & Qt.MouseButton.LeftButton and self.drag_position:
261
- self.move(event.globalPosition().toPoint() - self.drag_position)
262
- event.accept()
263
-
264
- def mouseReleaseEvent(self, event):
265
- self.drag_position = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/views/MainWindowView.py CHANGED
@@ -1,6 +1,6 @@
1
  from PyQt6.QtWidgets import (
2
  QMainWindow, QPushButton, QWidget, QVBoxLayout,
3
- QHBoxLayout, QLabel, QFrame, QMenu,
4
  )
5
  from PyQt6.QtGui import QIcon, QAction
6
  from PyQt6.QtCore import Qt
@@ -17,7 +17,20 @@ class MainWindowView(QMainWindow, LoggingMixin):
17
  QMainWindow.__init__(self)
18
  LoggingMixin.__init__(self)
19
  self.settings = global_settings
 
 
20
  self.action_toggle_theme = QAction("Toggle Theme", self)
 
 
 
 
 
 
 
 
 
 
 
21
  self._init_ui()
22
  self.oldPos = None
23
 
@@ -47,26 +60,20 @@ class MainWindowView(QMainWindow, LoggingMixin):
47
  self.log_debug(f"Window initialized at position ({x}, {y}) with size {final_size}")
48
 
49
  def _init_window_properties(self) -> None:
50
- self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
51
- self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
52
- self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
53
-
54
  toolbars = self.findChildren(QtWidgets.QToolBar)
55
  for toolbar in toolbars:
56
  toolbar.hide()
57
 
58
  def _init_ui_elements(self) -> None:
59
  self._init_menuBar()
60
- self._init_custom_title_bar()
61
 
62
  main_widget = QWidget()
63
  main_layout = QVBoxLayout(main_widget)
64
  main_layout.setContentsMargins(0, 0, 0, 0)
65
  main_layout.setSpacing(0)
66
 
67
- main_layout.addWidget(self.title_bar, 0)
68
- main_layout.addWidget(self._init_divider(), 0)
69
-
70
  # Create and set up tab container
71
  tab_container = QWidget()
72
  tab_container_layout = QVBoxLayout(tab_container)
@@ -93,11 +100,7 @@ class MainWindowView(QMainWindow, LoggingMixin):
93
  self.setCentralWidget(main_widget)
94
 
95
  def _init_menuBar(self) -> None:
96
- self.action_change_database_directory = self._find_widget("actChangeDatabaseDirectory", QAction)
97
- self.action_open_genome_browser = self._find_widget("actOpenGenomeBrowser", QAction)
98
- self.action_open_repository = self._find_widget("actionGoToCASPERRepository", QAction)
99
- self.action_open_NCBI_BLAST = self._find_widget("actionGoToNCBIBLAST", QAction)
100
- self.action_open_NCBI = self._find_widget("actGoToNCBI", QAction)
101
 
102
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
103
  """Find a widget by name and type"""
@@ -107,143 +110,8 @@ class MainWindowView(QMainWindow, LoggingMixin):
107
  return widget
108
 
109
  def _init_custom_title_bar(self) -> None:
110
- self.title_bar = QWidget(self)
111
- self.title_bar.setObjectName("custom_title_bar")
112
- self.title_bar.setFixedHeight(32)
113
-
114
- # Create the main horizontal layout for the title bar
115
- layout = QHBoxLayout(self.title_bar)
116
- layout.setContentsMargins(10, 0, 10, 0)
117
- layout.setSpacing(5)
118
-
119
- # ----- Window Control Buttons -----
120
- self.minimize_window_button = QPushButton("-", self.title_bar)
121
- self.minimize_window_button.setObjectName("minimize_window_button")
122
- self.minimize_window_button.setFixedSize(20, 20)
123
-
124
- self.maximize_window_button = QPushButton("⛶", self.title_bar)
125
- self.maximize_window_button.setObjectName("maximize_window_button")
126
- self.maximize_window_button.setFixedSize(20, 20)
127
-
128
- self.close_window_button = QPushButton("✕", self.title_bar)
129
- self.close_window_button.setObjectName("close_window_button")
130
- self.close_window_button.setFixedSize(20, 20)
131
-
132
- button_style = """
133
- QPushButton {
134
- padding: 0px;
135
- margin: 0px;
136
- line-height: 20px;
137
- text-align: center;
138
- }
139
- """
140
- self.minimize_window_button.setStyleSheet(button_style)
141
- self.maximize_window_button.setStyleSheet(button_style)
142
- self.close_window_button.setStyleSheet(button_style)
143
-
144
- # ----- Left Widget (Minimize, Maximize, Close) -----
145
- left_widget = QWidget()
146
- left_layout = QHBoxLayout(left_widget)
147
- left_layout.setContentsMargins(0, 0, 0, 0)
148
- left_layout.setSpacing(5)
149
- left_layout.addWidget(self.close_window_button)
150
- left_layout.addWidget(self.minimize_window_button)
151
- left_layout.addWidget(self.maximize_window_button)
152
-
153
- # ----- Add Button with Dropdown -----
154
- self.add_button = QPushButton(self.title_bar)
155
- self.add_button.setObjectName("add_button")
156
- self.add_button.setFixedSize(20, 20)
157
-
158
- # Create the dropdown menu
159
- self.add_menu = QMenu(self.add_button)
160
- self.add_menu.setObjectName("add_menu")
161
-
162
- # Add actions to the menu
163
- self.action_new_genome = self.add_menu.addAction("New Genome")
164
- self.action_new_endonuclease = self.add_menu.addAction("New Endonuclease")
165
-
166
- # Set the menu for the button
167
- self.add_button.setMenu(self.add_menu)
168
- self.add_button.setStyleSheet("""
169
- QPushButton {
170
- padding: 0px;
171
- margin: 0px;
172
- text-align: center;
173
- border: none;
174
- }
175
- QPushButton::menu-indicator {
176
- width: 0px;
177
- }
178
- """)
179
-
180
- # Initial icon will be set in update_plus_icon method
181
- self.update_plus_icon()
182
-
183
- # ----- Settings Button with Dropdown -----
184
- self.settings_button = QPushButton(self.title_bar)
185
- self.settings_button.setObjectName("settings_button")
186
- self.settings_button.setFixedSize(20, 20)
187
-
188
- # Create the settings dropdown menu
189
- self.settings_menu = QMenu(self.settings_button)
190
- self.settings_menu.setObjectName("settings_menu")
191
-
192
- # Update the theme icon and add the action to the menu
193
- self.update_theme_icon()
194
- self.settings_menu.addAction(self.action_toggle_theme)
195
-
196
- # Set the menu for the button
197
- self.settings_button.setMenu(self.settings_menu)
198
- self.settings_button.setStyleSheet("""
199
- QPushButton {
200
- padding: 0px;
201
- margin: 0px;
202
- text-align: center;
203
- border: none;
204
- }
205
- QPushButton::menu-indicator {
206
- width: 0px;
207
- }
208
- """)
209
-
210
- # Initial icon will be set in update_settings_icon method
211
- self.update_settings_icon()
212
-
213
- # ----- Right Widget (Add Button + Settings Button + Stretch) -----
214
- right_widget = QWidget()
215
- right_layout = QHBoxLayout(right_widget)
216
- right_layout.setContentsMargins(0, 0, 0, 0)
217
- right_layout.setSpacing(5)
218
-
219
- # Add a stretch to push the buttons to the far right within right_widget
220
- right_layout.addStretch()
221
- right_layout.addWidget(self.add_button)
222
- right_layout.addWidget(self.settings_button)
223
-
224
- # Adjust left_widget to calculate its required width
225
- left_widget.adjustSize()
226
- left_width = left_widget.sizeHint().width()
227
-
228
- # Set right_widget's fixed width to match left_widget's width
229
- right_widget.setFixedWidth(left_width)
230
-
231
- self.title_label = QLabel("CASPER", self.title_bar)
232
- self.title_label.setObjectName("title_label")
233
- self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text in the label
234
-
235
- # Add Widgets to the Main Title Bar Layout
236
- layout.addWidget(left_widget)
237
- layout.addStretch(1)
238
- layout.addWidget(self.title_label)
239
- layout.addStretch(1)
240
- layout.addWidget(right_widget)
241
-
242
- # Add mouse tracking to the title bar
243
- self.title_bar.mousePressEvent = self.mousePressEvent
244
- self.title_bar.mouseMoveEvent = self.mouseMoveEvent
245
- self.title_bar.mouseReleaseEvent = self.mouseReleaseEvent
246
- self.title_bar.setMouseTracking(True)
247
 
248
  def _init_divider(self):
249
  divider = QFrame()
@@ -352,31 +220,6 @@ class MainWindowView(QMainWindow, LoggingMixin):
352
  QMenu {{ background-color: {theme['menu_bg_color']}; }}
353
  QMenu::item:selected {{ background-color: {theme['menu_item_hover_bg_color']}; }}
354
  QFrame#custom_divider {{ border-bottom: 1px solid {theme['divider_color']}; }}
355
-
356
- QPushButton#add_button {{
357
- background-color: {theme['button_bg_color']};
358
- color: {theme['fg_color']};
359
- border: 1px solid {theme['button_border_color']};
360
- padding: 0px;
361
- font-size: 16px;
362
- line-height: 20px;
363
- }}
364
-
365
- QPushButton#add_button:hover {{
366
- background-color: {theme['button_hover_bg_color']};
367
- }}
368
-
369
- QMenu {{
370
- background-color: {theme['menu_bg_color']};
371
- color: {theme['menu_text_color']};
372
- border: 1px solid {theme['button_border_color']};
373
- padding: 5px;
374
- }}
375
-
376
- QMenu::item:selected {{
377
- background-color: {theme['menu_item_hover_bg_color']};
378
- color: {theme['menu_hover_text_color']};
379
- }}
380
  """)
381
 
382
  # Set the tab widget stylesheet
@@ -412,49 +255,8 @@ class MainWindowView(QMainWindow, LoggingMixin):
412
  }}
413
  """)
414
 
415
- # Update the add button styling in the theme
416
- self.add_button.setStyleSheet(f"""
417
- QPushButton {{
418
- padding: 0px;
419
- margin: 0px;
420
- line-height: 0px;
421
- text-align: center;
422
- border: none;
423
- font-size: 14px;
424
- color: {theme['fg_color']};
425
- }}
426
- QPushButton:hover {{
427
- background-color: {theme['button_hover_bg_color']};
428
- }}
429
- QPushButton::menu-indicator {{
430
- width: 0px;
431
- }}
432
- """)
433
-
434
- # Update the settings button styling
435
- self.settings_button.setStyleSheet(f"""
436
- QPushButton {{
437
- padding: 0px;
438
- margin: 0px;
439
- line-height: 0px;
440
- text-align: center;
441
- border: none;
442
- font-size: 14px;
443
- color: {theme['fg_color']};
444
- background-color: transparent;
445
- }}
446
- QPushButton:hover {{
447
- background-color: {theme['button_hover_bg_color']};
448
- }}
449
- QPushButton::menu-indicator {{
450
- width: 0px;
451
- }}
452
- """)
453
-
454
- # Update icons
455
  self.update_theme_icon()
456
- self.update_plus_icon()
457
- self.update_settings_icon()
458
 
459
  def mousePressEvent(self, event):
460
  """Handle mouse press events for window dragging"""
@@ -471,4 +273,30 @@ class MainWindowView(QMainWindow, LoggingMixin):
471
  def mouseReleaseEvent(self, event):
472
  """Handle mouse release events for window dragging"""
473
  if event.button() == Qt.MouseButton.LeftButton:
474
- self.oldPos = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from PyQt6.QtWidgets import (
2
  QMainWindow, QPushButton, QWidget, QVBoxLayout,
3
+ QHBoxLayout, QLabel, QFrame, QMenu, QToolBar,
4
  )
5
  from PyQt6.QtGui import QIcon, QAction
6
  from PyQt6.QtCore import Qt
 
17
  QMainWindow.__init__(self)
18
  LoggingMixin.__init__(self)
19
  self.settings = global_settings
20
+
21
+ # Initialize all actions
22
  self.action_toggle_theme = QAction("Toggle Theme", self)
23
+ self.action_new_genome = QAction("New Genome", self)
24
+ self.action_new_endonuclease = QAction("New Endonuclease", self)
25
+ self.action_change_database_directory = QAction("Change Database Directory", self)
26
+ self.action_open_repository = QAction("Open Repository", self)
27
+ self.action_open_NCBI = QAction("Open NCBI", self)
28
+ self.action_open_NCBI_BLAST = QAction("Open NCBI BLAST", self)
29
+
30
+ # Add keyboard shortcuts
31
+ self.action_new_genome.setShortcut("Ctrl+N") # Will be shown as Cmd+N on macOS
32
+ self.action_toggle_theme.setShortcut("Ctrl+T") # Will be shown as Cmd+T on macOS
33
+
34
  self._init_ui()
35
  self.oldPos = None
36
 
 
60
  self.log_debug(f"Window initialized at position ({x}, {y}) with size {final_size}")
61
 
62
  def _init_window_properties(self) -> None:
63
+ # Remove frameless window hint to show native window controls
 
 
 
64
  toolbars = self.findChildren(QtWidgets.QToolBar)
65
  for toolbar in toolbars:
66
  toolbar.hide()
67
 
68
  def _init_ui_elements(self) -> None:
69
  self._init_menuBar()
70
+ self._setup_native_menu_bar()
71
 
72
  main_widget = QWidget()
73
  main_layout = QVBoxLayout(main_widget)
74
  main_layout.setContentsMargins(0, 0, 0, 0)
75
  main_layout.setSpacing(0)
76
 
 
 
 
77
  # Create and set up tab container
78
  tab_container = QWidget()
79
  tab_container_layout = QVBoxLayout(tab_container)
 
100
  self.setCentralWidget(main_widget)
101
 
102
  def _init_menuBar(self) -> None:
103
+ pass
 
 
 
 
104
 
105
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
106
  """Find a widget by name and type"""
 
110
  return widget
111
 
112
  def _init_custom_title_bar(self) -> None:
113
+ # Remove custom title bar implementation
114
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def _init_divider(self):
117
  divider = QFrame()
 
220
  QMenu {{ background-color: {theme['menu_bg_color']}; }}
221
  QMenu::item:selected {{ background-color: {theme['menu_item_hover_bg_color']}; }}
222
  QFrame#custom_divider {{ border-bottom: 1px solid {theme['divider_color']}; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  """)
224
 
225
  # Set the tab widget stylesheet
 
255
  }}
256
  """)
257
 
258
+ # Update theme icon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  self.update_theme_icon()
 
 
260
 
261
  def mousePressEvent(self, event):
262
  """Handle mouse press events for window dragging"""
 
273
  def mouseReleaseEvent(self, event):
274
  """Handle mouse release events for window dragging"""
275
  if event.button() == Qt.MouseButton.LeftButton:
276
+ self.oldPos = None
277
+
278
+ def _setup_native_menu_bar(self) -> None:
279
+ """Setup the native menu bar for macOS"""
280
+ menubar = self.menuBar
281
+
282
+ # File Menu
283
+ file_menu = menubar.addMenu('File')
284
+ file_menu.addAction(self.action_change_database_directory)
285
+
286
+ # Add Menu (for New Genome and New Endonuclease)
287
+ add_menu = menubar.addMenu('Add')
288
+ add_menu.addAction(self.action_new_genome)
289
+ add_menu.addAction(self.action_new_endonuclease)
290
+
291
+ # Settings Menu
292
+ settings_menu = menubar.addMenu('Settings')
293
+ settings_menu.addAction(self.action_toggle_theme)
294
+
295
+ # Help Menu
296
+ help_menu = menubar.addMenu('Help')
297
+ help_menu.addAction(self.action_open_repository)
298
+ help_menu.addAction(self.action_open_NCBI)
299
+ help_menu.addAction(self.action_open_NCBI_BLAST)
300
+
301
+ # Make sure menu bar is visible
302
+ menubar.setNativeMenuBar(True) # Use native macOS menu bar
src/views/MultitargetingWindowView.py CHANGED
@@ -2,18 +2,24 @@ from typing import Optional
2
  from PyQt6 import QtWidgets, uic, QtGui
3
  from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
4
  from PyQt6.QtCore import Qt
5
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
6
  from matplotlib.figure import Figure
7
  from matplotlib.ticker import MaxNLocator
8
  from utils.ui import show_error
 
9
 
10
  class MultitargetingWindowView(QtWidgets.QMainWindow):
11
  def __init__(self, global_settings):
 
12
  super().__init__()
13
  self.settings = global_settings
14
  self.logger = self.settings.get_logger()
15
 
 
16
  self.init_ui()
 
 
 
17
 
18
  def init_ui(self):
19
  try:
@@ -134,26 +140,42 @@ class MultitargetingWindowView(QtWidgets.QMainWindow):
134
  self.table_seeds.resizeColumnsToContents()
135
 
136
  def setup_plots(self):
137
- """Initialize the matplotlib plots"""
138
- self.repeats_vs_seed_canvas = MplCanvas(self, width=8, height=6)
139
- self.sequences_vs_repeats_canvas = MplCanvas(self, width=8, height=6)
140
- self.repeat_vs_chromosome_canvas = MplCanvas(self, width=8, height=6)
141
-
142
- # Add canvases to their respective layouts without toolbars
143
- for plot_widget, canvas in [
144
- (self.plot_repeats_vs_seed, self.repeats_vs_seed_canvas),
145
- (self.plot_sequences_vs_repeats, self.sequences_vs_repeats_canvas),
146
- (self.plot_repeat_vs_chromosome, self.repeat_vs_chromosome_canvas)
147
- ]:
148
- layout = QtWidgets.QVBoxLayout(plot_widget)
149
- layout.setContentsMargins(0, 0, 0, 0) # Reduce margins
150
- layout.addWidget(canvas)
 
 
 
 
151
 
152
  def update_plots(self, repeats_data, sequences_data, chromosome_data):
153
  """Update all plots with new data"""
 
 
 
154
  self._update_repeats_vs_seed_plot(repeats_data)
 
 
 
155
  self._update_sequences_vs_repeats_plot(sequences_data)
 
 
 
156
  self._update_repeat_vs_chromosome_plot(chromosome_data)
 
 
 
157
 
158
  def _update_repeats_vs_seed_plot(self, data):
159
  """Update the repeats vs seed line plot"""
 
2
  from PyQt6 import QtWidgets, uic, QtGui
3
  from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
4
  from PyQt6.QtCore import Qt
5
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
6
  from matplotlib.figure import Figure
7
  from matplotlib.ticker import MaxNLocator
8
  from utils.ui import show_error
9
+ import time
10
 
11
  class MultitargetingWindowView(QtWidgets.QMainWindow):
12
  def __init__(self, global_settings):
13
+ start_time = time.time()
14
  super().__init__()
15
  self.settings = global_settings
16
  self.logger = self.settings.get_logger()
17
 
18
+ init_ui_start = time.time()
19
  self.init_ui()
20
+ self.logger.debug(f"UI initialization took: {time.time() - init_ui_start:.2f} seconds")
21
+
22
+ self.logger.debug(f"Total view initialization took: {time.time() - start_time:.2f} seconds")
23
 
24
  def init_ui(self):
25
  try:
 
140
  self.table_seeds.resizeColumnsToContents()
141
 
142
  def setup_plots(self):
143
+ """Initialize the matplotlib plots only when needed"""
144
+ if not hasattr(self, 'repeats_vs_seed_canvas'):
145
+ self.repeats_vs_seed_canvas = MplCanvas(self, width=8, height=6)
146
+ layout = QtWidgets.QVBoxLayout(self.plot_repeats_vs_seed)
147
+ layout.setContentsMargins(0, 0, 0, 0)
148
+ layout.addWidget(self.repeats_vs_seed_canvas)
149
+
150
+ if not hasattr(self, 'sequences_vs_repeats_canvas'):
151
+ self.sequences_vs_repeats_canvas = MplCanvas(self, width=8, height=6)
152
+ layout = QtWidgets.QVBoxLayout(self.plot_sequences_vs_repeats)
153
+ layout.setContentsMargins(0, 0, 0, 0)
154
+ layout.addWidget(self.sequences_vs_repeats_canvas)
155
+
156
+ if not hasattr(self, 'repeat_vs_chromosome_canvas'):
157
+ self.repeat_vs_chromosome_canvas = MplCanvas(self, width=8, height=6)
158
+ layout = QtWidgets.QVBoxLayout(self.plot_repeat_vs_chromosome)
159
+ layout.setContentsMargins(0, 0, 0, 0)
160
+ layout.addWidget(self.repeat_vs_chromosome_canvas)
161
 
162
  def update_plots(self, repeats_data, sequences_data, chromosome_data):
163
  """Update all plots with new data"""
164
+ start_time = time.time()
165
+
166
+ plot1_start = time.time()
167
  self._update_repeats_vs_seed_plot(repeats_data)
168
+ self.logger.debug(f"Repeats vs seed plot update took: {time.time() - plot1_start:.2f} seconds")
169
+
170
+ plot2_start = time.time()
171
  self._update_sequences_vs_repeats_plot(sequences_data)
172
+ self.logger.debug(f"Sequences vs repeats plot update took: {time.time() - plot2_start:.2f} seconds")
173
+
174
+ plot3_start = time.time()
175
  self._update_repeat_vs_chromosome_plot(chromosome_data)
176
+ self.logger.debug(f"Chromosome plot update took: {time.time() - plot3_start:.2f} seconds")
177
+
178
+ self.logger.debug(f"Total plot updates took: {time.time() - start_time:.2f} seconds")
179
 
180
  def _update_repeats_vs_seed_plot(self, data):
181
  """Update the repeats vs seed line plot"""
src/views/NCBIWindowView.py CHANGED
@@ -36,7 +36,6 @@ class NCBIWindowView(QtWidgets.QMainWindow):
36
  self._init_ui_components()
37
 
38
  self._is_initialized = True
39
- self.logger.debug("NCBI Window initialization completed")
40
 
41
  # Emit signal after everything is initialized
42
  self.initialization_complete.emit()
@@ -61,9 +60,17 @@ class NCBIWindowView(QtWidgets.QMainWindow):
61
  self.line_edit_strain = self._find_widget("ledStrain", QtWidgets.QLineEdit)
62
  self.line_edit_max_results = self._find_widget("ledMaxResults", QtWidgets.QLineEdit)
63
  self.check_box_complete_genomes_only = self._find_widget("chkCompleteGenomesOnly", QtWidgets.QCheckBox)
 
 
 
 
 
 
 
64
 
65
  # Set default values
66
  self.line_edit_max_results.setText("100")
 
67
 
68
  except Exception as e:
69
  self.logger.error(f"Error initializing Step 1: {str(e)}")
@@ -151,7 +158,8 @@ class NCBIWindowView(QtWidgets.QMainWindow):
151
  'refseq': self.radio_button_collections_refseq.isChecked(),
152
  'genbank': self.radio_button_collections_genbank.isChecked(),
153
  'fna': self.check_box_file_types_fna.isChecked(),
154
- 'gbff': self.check_box_file_types_gbff.isChecked()
 
155
  }
156
 
157
  def get_selected_rows(self):
 
36
  self._init_ui_components()
37
 
38
  self._is_initialized = True
 
39
 
40
  # Emit signal after everything is initialized
41
  self.initialization_complete.emit()
 
60
  self.line_edit_strain = self._find_widget("ledStrain", QtWidgets.QLineEdit)
61
  self.line_edit_max_results = self._find_widget("ledMaxResults", QtWidgets.QLineEdit)
62
  self.check_box_complete_genomes_only = self._find_widget("chkCompleteGenomesOnly", QtWidgets.QCheckBox)
63
+ self.combo_box_database = self._find_widget("cmbDatabase", QtWidgets.QComboBox)
64
+
65
+ # Initialize database options
66
+ self.combo_box_database.addItems([
67
+ "NCBI GenBank",
68
+ "ENA (European Nucleotide Archive)"
69
+ ])
70
 
71
  # Set default values
72
  self.line_edit_max_results.setText("100")
73
+ self.combo_box_database.setCurrentText("NCBI GenBank")
74
 
75
  except Exception as e:
76
  self.logger.error(f"Error initializing Step 1: {str(e)}")
 
158
  'refseq': self.radio_button_collections_refseq.isChecked(),
159
  'genbank': self.radio_button_collections_genbank.isChecked(),
160
  'fna': self.check_box_file_types_fna.isChecked(),
161
+ 'gbff': self.check_box_file_types_gbff.isChecked(),
162
+ 'database': self.combo_box_database.currentText()
163
  }
164
 
165
  def get_selected_rows(self):
src/views/StartupWindowView.py CHANGED
@@ -42,7 +42,6 @@ class StartupWindowView(QtWidgets.QMainWindow):
42
 
43
  def _init_boxlayvBottom(self):
44
  self.push_button_go_to_home_or_new_genome = self._find_widget('pbtnGoToHomeOrNewGenome', QtWidgets.QPushButton)
45
- self.push_button_go_to_home_or_new_genome.clicked.connect(self._on_go_to_home_or_new_genome_clicked)
46
 
47
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
48
  widget = self.findChild(widget_type, name)
@@ -81,8 +80,4 @@ class StartupWindowView(QtWidgets.QMainWindow):
81
  self.label_db_status.show()
82
  self.label_db_status.setStyleSheet("color: red;")
83
  self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
84
- self.push_button_go_to_home_or_new_genome.setEnabled(True)
85
-
86
- def _on_go_to_home_or_new_genome_clicked(self):
87
- if self.push_button_go_to_home_or_new_genome.text() == "Analyze a New Genome":
88
- self.open_new_genome_requested.emit()
 
42
 
43
  def _init_boxlayvBottom(self):
44
  self.push_button_go_to_home_or_new_genome = self._find_widget('pbtnGoToHomeOrNewGenome', QtWidgets.QPushButton)
 
45
 
46
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
47
  widget = self.findChild(widget_type, name)
 
80
  self.label_db_status.show()
81
  self.label_db_status.setStyleSheet("color: red;")
82
  self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
83
+ self.push_button_go_to_home_or_new_genome.setEnabled(True)
 
 
 
 
src/views/ViewTargetsView.py CHANGED
@@ -1,11 +1,11 @@
1
  from typing import Optional
2
- from PyQt6 import QtWidgets, uic, QtCore
3
  from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
4
  from PyQt6.QtGui import QTextDocument
5
  from PyQt6.QtCore import Qt, pyqtSignal
6
  from utils.ui import show_error
7
- import time
8
  import traceback
 
9
 
10
  class ViewTargetsView(QtWidgets.QMainWindow):
11
  # Define the signal
@@ -49,6 +49,9 @@ class ViewTargetsView(QtWidgets.QMainWindow):
49
  self.table_guides.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
50
  self.table_guides.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
51
 
 
 
 
52
  # Enable horizontal scrolling
53
  self.table_guides.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel)
54
  self.table_guides.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
@@ -62,7 +65,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
62
  # Set resize mode for header
63
  header = self.table_guides.horizontalHeader()
64
  header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Interactive)
65
- header.setStretchLastSection(False) # Don't stretch the last section
66
 
67
  # Set minimum section size to prevent columns from becoming too narrow
68
  header.setMinimumSectionSize(80)
@@ -79,9 +82,32 @@ class ViewTargetsView(QtWidgets.QMainWindow):
79
  self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
80
  self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
81
  self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
 
82
 
83
  self.text_edit_gene_viewer.setReadOnly(True)
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
86
  widget = self.findChild(widget_type, name)
87
  if widget is None:
@@ -89,9 +115,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
89
  return widget
90
 
91
  def display_guides_in_table(self, guides):
92
- """Ultra-fast guide display with virtual table and minimal UI updates"""
93
  try:
94
- # Store complete set of guides
95
  self._all_guides = guides
96
 
97
  selected_text = self.combo_box_gene.currentText()
@@ -161,14 +185,14 @@ class ViewTargetsView(QtWidgets.QMainWindow):
161
  location = guide['location']
162
  start_pos = location.split('-')[0] if '-' in location else location
163
 
164
- # Create and set basic items
165
  items = [
166
- (0, QTableWidgetItem(start_pos)), # Only show start position
167
  (1, QTableWidgetItem(guide['endonuclease'])),
168
  (2, QTableWidgetItem(guide['sequence'])),
169
  (3, QTableWidgetItem(guide['strand'])),
170
  (4, QTableWidgetItem(guide['pam'])),
171
- (5, QTableWidgetItem(str(guide['score']))),
172
  (6, QTableWidgetItem("--.--")) # Off-target placeholder
173
  ]
174
 
@@ -185,7 +209,8 @@ class ViewTargetsView(QtWidgets.QMainWindow):
185
 
186
  # Add Azimuth score if column exists
187
  if azimuth_index is not None and 'azimuth_score' in guide:
188
- azimuth_item = QTableWidgetItem(str(guide.get('azimuth_score', 0)))
 
189
  self.table_guides.setItem(row, azimuth_index, azimuth_item)
190
 
191
  # Updated column widths
@@ -222,6 +247,25 @@ class ViewTargetsView(QtWidgets.QMainWindow):
222
  self.logger.error(f"Error in display_guides: {str(e)}")
223
  show_error(self.settings, "Error displaying guides", str(e))
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  def _handle_scroll_virtual(self, value, total_rows, row_height, buffer_rows):
226
  try:
227
  if not hasattr(self, '_all_guides') or not self._all_guides:
@@ -242,16 +286,27 @@ class ViewTargetsView(QtWidgets.QMainWindow):
242
  if row < len(self._all_guides) and not self.table_guides.item(row, 0):
243
  guide = self._all_guides[row]
244
 
245
- # Create and set items efficiently
246
- for col, value in enumerate([
247
- guide['location'], guide['endonuclease'],
248
- guide['sequence'], guide['strand'], guide['pam'],
249
- guide['score'], "--.--"
250
- ]):
251
- item = QTableWidgetItem(str(value))
 
 
 
 
 
 
 
 
 
 
252
  item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
253
  self.table_guides.setItem(row, col, item)
254
 
 
255
  if not self.table_guides.cellWidget(row, 7):
256
  details_button = QtWidgets.QPushButton("Details")
257
  self.table_guides.setCellWidget(row, 7, details_button)
@@ -328,7 +383,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
328
 
329
  def set_combo_box_gene(self, genes):
330
  try:
331
-
332
  # Disable UI updates
333
  self.combo_box_gene.blockSignals(True)
334
  self.combo_box_gene.setUpdatesEnabled(False)
@@ -339,16 +393,19 @@ class ViewTargetsView(QtWidgets.QMainWindow):
339
  # Debug logging
340
  self.logger.debug(f"Received {len(genes)} genes")
341
 
 
 
 
342
  # Add items in a single batch
343
- if genes:
344
  # Pre-allocate size
345
- self.combo_box_gene.insertItems(0, genes)
346
 
347
  # Set first item without triggering updates
348
  if self.combo_box_gene.count() > 0:
349
  self.combo_box_gene.setCurrentIndex(0)
350
 
351
- self.logger.debug(f"Added {len(genes)} genes to combo box")
352
 
353
  # Re-enable UI updates
354
  self.combo_box_gene.setUpdatesEnabled(True)
@@ -370,15 +427,24 @@ class ViewTargetsView(QtWidgets.QMainWindow):
370
  except Exception as e:
371
  self.logger.error(f"Error setting gene viewer text: {str(e)}")
372
 
373
- def update_gene_info(self, info):
374
- # Implement this method if you have a widget to display gene info
375
- pass
376
-
377
- def update_gene_viewer(self, sequence):
378
  self.text_edit_gene_viewer.clear()
379
  doc = QTextDocument()
380
  doc.setHtml(sequence)
381
  self.text_edit_gene_viewer.setDocument(doc)
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  def select_all_guides(self, select):
384
  for row in range(self.table_guides.rowCount()):
@@ -491,4 +557,42 @@ class ViewTargetsView(QtWidgets.QMainWindow):
491
 
492
  except Exception as e:
493
  self.logger.error(f"Error showing details: {str(e)}")
494
- show_error(self.settings, "Error showing details", str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from typing import Optional
2
+ from PyQt6 import QtWidgets, uic
3
  from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
4
  from PyQt6.QtGui import QTextDocument
5
  from PyQt6.QtCore import Qt, pyqtSignal
6
  from utils.ui import show_error
 
7
  import traceback
8
+ from views.DNAFeatureViewer import DNAFeatureViewer
9
 
10
  class ViewTargetsView(QtWidgets.QMainWindow):
11
  # Define the signal
 
49
  self.table_guides.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
50
  self.table_guides.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
51
 
52
+ # Enable sorting
53
+ self.table_guides.setSortingEnabled(True)
54
+
55
  # Enable horizontal scrolling
56
  self.table_guides.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel)
57
  self.table_guides.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
 
65
  # Set resize mode for header
66
  header = self.table_guides.horizontalHeader()
67
  header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Interactive)
68
+ header.setStretchLastSection(False)
69
 
70
  # Set minimum section size to prevent columns from becoming too narrow
71
  header.setMinimumSectionSize(80)
 
82
  self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
83
  self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
84
  self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
85
+ self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
86
 
87
  self.text_edit_gene_viewer.setReadOnly(True)
88
 
89
+ # Create DNA feature viewer
90
+ self.dna_feature_viewer = DNAFeatureViewer()
91
+
92
+ # Get the layout of the gene viewer group
93
+ gene_viewer_group = self.findChild(QtWidgets.QGroupBox, 'grpGeneViewer')
94
+ gene_viewer_layout = gene_viewer_group.layout()
95
+
96
+ # Find the row index of the text editor
97
+ text_editor_row = -1
98
+ for i in range(gene_viewer_layout.rowCount()):
99
+ item = gene_viewer_layout.itemAtPosition(i, 0)
100
+ if item and item.widget() == self.text_edit_gene_viewer:
101
+ text_editor_row = i
102
+ break
103
+
104
+ if text_editor_row != -1:
105
+ # Insert DNA feature viewer above the text editor
106
+ gene_viewer_layout.addWidget(self.dna_feature_viewer, text_editor_row, 0, 1, -1)
107
+
108
+ # Connect signals
109
+ self.dna_feature_viewer.sequence_selected.connect(self._on_sequence_selected)
110
+
111
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
112
  widget = self.findChild(widget_type, name)
113
  if widget is None:
 
115
  return widget
116
 
117
  def display_guides_in_table(self, guides):
 
118
  try:
 
119
  self._all_guides = guides
120
 
121
  selected_text = self.combo_box_gene.currentText()
 
185
  location = guide['location']
186
  start_pos = location.split('-')[0] if '-' in location else location
187
 
188
+ # Create items with proper data roles for sorting
189
  items = [
190
+ (0, self._create_sortable_item(start_pos, int(start_pos))), # Location as number
191
  (1, QTableWidgetItem(guide['endonuclease'])),
192
  (2, QTableWidgetItem(guide['sequence'])),
193
  (3, QTableWidgetItem(guide['strand'])),
194
  (4, QTableWidgetItem(guide['pam'])),
195
+ (5, self._create_sortable_item(str(guide['score']), float(guide['score']))), # Score as number
196
  (6, QTableWidgetItem("--.--")) # Off-target placeholder
197
  ]
198
 
 
209
 
210
  # Add Azimuth score if column exists
211
  if azimuth_index is not None and 'azimuth_score' in guide:
212
+ azimuth_score = float(guide['azimuth_score'])
213
+ azimuth_item = self._create_sortable_item(str(azimuth_score), azimuth_score)
214
  self.table_guides.setItem(row, azimuth_index, azimuth_item)
215
 
216
  # Updated column widths
 
247
  self.logger.error(f"Error in display_guides: {str(e)}")
248
  show_error(self.settings, "Error displaying guides", str(e))
249
 
250
+ def _create_sortable_item(self, display_text, sort_value):
251
+ """Create a table item that displays text but sorts by numeric value"""
252
+ item = QTableWidgetItem()
253
+ item.setData(Qt.ItemDataRole.DisplayRole, sort_value) # Use raw value for display
254
+ item.setData(Qt.ItemDataRole.EditRole, sort_value) # Used for sorting
255
+
256
+ # Format display text based on value type
257
+ if isinstance(sort_value, (int, float)):
258
+ if isinstance(sort_value, int):
259
+ # For integers (like positions), show full number
260
+ item.setText(f"{sort_value:d}")
261
+ else:
262
+ # For floats (like scores), show with 2 decimal places
263
+ item.setText(f"{sort_value:.2f}")
264
+ else:
265
+ item.setText(str(sort_value))
266
+
267
+ return item
268
+
269
  def _handle_scroll_virtual(self, value, total_rows, row_height, buffer_rows):
270
  try:
271
  if not hasattr(self, '_all_guides') or not self._all_guides:
 
286
  if row < len(self._all_guides) and not self.table_guides.item(row, 0):
287
  guide = self._all_guides[row]
288
 
289
+ # Extract start position from location
290
+ location = guide['location']
291
+ start_pos = location.split('-')[0] if '-' in location else location
292
+
293
+ # Create items with proper data roles for sorting
294
+ items = [
295
+ (0, self._create_sortable_item(start_pos, int(start_pos))), # Location as number
296
+ (1, QTableWidgetItem(guide['endonuclease'])),
297
+ (2, QTableWidgetItem(guide['sequence'])),
298
+ (3, QTableWidgetItem(guide['strand'])),
299
+ (4, QTableWidgetItem(guide['pam'])),
300
+ (5, self._create_sortable_item(str(guide['score']), float(guide['score']))), # Score as number
301
+ (6, QTableWidgetItem("--.--")) # Off-target placeholder
302
+ ]
303
+
304
+ # Set items with flags
305
+ for col, item in items:
306
  item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
307
  self.table_guides.setItem(row, col, item)
308
 
309
+ # Add details button
310
  if not self.table_guides.cellWidget(row, 7):
311
  details_button = QtWidgets.QPushButton("Details")
312
  self.table_guides.setCellWidget(row, 7, details_button)
 
383
 
384
  def set_combo_box_gene(self, genes):
385
  try:
 
386
  # Disable UI updates
387
  self.combo_box_gene.blockSignals(True)
388
  self.combo_box_gene.setUpdatesEnabled(False)
 
393
  # Debug logging
394
  self.logger.debug(f"Received {len(genes)} genes")
395
 
396
+ # Use a set to ensure uniqueness
397
+ unique_genes = list(set(genes))
398
+
399
  # Add items in a single batch
400
+ if unique_genes:
401
  # Pre-allocate size
402
+ self.combo_box_gene.insertItems(0, unique_genes)
403
 
404
  # Set first item without triggering updates
405
  if self.combo_box_gene.count() > 0:
406
  self.combo_box_gene.setCurrentIndex(0)
407
 
408
+ self.logger.debug(f"Added {len(unique_genes)} unique genes to combo box")
409
 
410
  # Re-enable UI updates
411
  self.combo_box_gene.setUpdatesEnabled(True)
 
427
  except Exception as e:
428
  self.logger.error(f"Error setting gene viewer text: {str(e)}")
429
 
430
+ def update_gene_viewer(self, sequence, features=None):
431
+ """Update both text editor and DNA feature viewer"""
432
+ # Update text editor
 
 
433
  self.text_edit_gene_viewer.clear()
434
  doc = QTextDocument()
435
  doc.setHtml(sequence)
436
  self.text_edit_gene_viewer.setDocument(doc)
437
+
438
+ # Get start position from line edit
439
+ try:
440
+ start_pos = int(self.line_edit_start_location.text())
441
+ except (ValueError, TypeError):
442
+ start_pos = 1
443
+
444
+ # Update DNA feature viewer
445
+ if features is None:
446
+ features = []
447
+ self.dna_feature_viewer.set_data(sequence, features, start_pos)
448
 
449
  def select_all_guides(self, select):
450
  for row in range(self.table_guides.rowCount()):
 
557
 
558
  except Exception as e:
559
  self.logger.error(f"Error showing details: {str(e)}")
560
+ show_error(self.settings, "Error showing details", str(e))
561
+
562
+ def _on_sequence_selected(self, start, end):
563
+ """Handle sequence selection in DNA feature viewer"""
564
+ self.line_edit_start_location.setText(str(start))
565
+ self.line_edit_stop_location.setText(str(end))
566
+
567
+ def highlight_guides_in_viewer(self, guides_to_highlight, sequence):
568
+ """Highlight guides in viewer"""
569
+ try:
570
+ for guide in guides_to_highlight:
571
+ sequence_to_find = guide['sequence']
572
+ strand = guide['strand']
573
+
574
+ if strand == '-':
575
+ sequence_to_find = str(Seq(sequence_to_find).reverse_complement())
576
+
577
+ sequence_upper = sequence.upper()
578
+ target_upper = sequence_to_find.upper()
579
+
580
+ pos = sequence_upper.find(target_upper)
581
+ if pos != -1:
582
+ # Set color based on strand
583
+ color = QColor(255, 0, 0, 100) if strand == '-' else QColor(0, 255, 0, 100)
584
+
585
+ # Highlight sequence in viewer
586
+ self.dna_feature_viewer.sequence_viewer.highlight_sequence(
587
+ pos,
588
+ pos + len(sequence_to_find) - 1,
589
+ color
590
+ )
591
+
592
+ except Exception as e:
593
+ self.logger.error(f"Error highlighting guides: {str(e)}")
594
+ show_error(self.settings, "Error highlighting guides", str(e))
595
+
596
+ def clear_highlights(self):
597
+ """Clear highlights in viewer"""
598
+ self.dna_feature_viewer.sequence_viewer.clear_highlights()